Skip to content
This repository has been archived by the owner on Apr 30, 2022. It is now read-only.

Migrate to MQTT from HTTPS polling #16

Draft
wants to merge 27 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e71d06e
Use the Glow App ID rather than prompting for one
unlobito Aug 12, 2020
bacb33a
glow: add retrieve_devices method
unlobito Aug 12, 2020
c453f1e
Merge branch 'master' into mqtt
unlobito May 15, 2021
b17515f
move InvalidAuth handling out of sensor
unlobito May 16, 2021
24bf8d7
calculate MQTT topic based on CAD ID
unlobito May 16, 2021
9f9c2f6
connect to MQTT server with discovered CAD ID
unlobito May 16, 2021
dd104d8
persist credentials in Glow object
unlobito May 17, 2021
3e7432a
WIP: route MQTT data to sensor object
unlobito May 17, 2021
84939dd
migrate to paho-mqtt
unlobito May 17, 2021
61408de
parse more MQTTPayload data
unlobito May 17, 2021
610f5de
MQTTPayload: docstrings
unlobito May 17, 2021
71af3ba
attempt gas readings
unlobito May 17, 2021
68d34e7
typo in supply_status key
unlobito May 17, 2021
db42193
subscribe to HILD and DCAD topics
unlobito May 17, 2021
6bdd399
address mypy errors
unlobito May 17, 2021
367d610
manifest: version 0.1.1
unlobito Jun 5, 2021
8ef0a46
use MQTT wildcard for topic
unlobito Jun 5, 2021
ce7bb34
chore: device_info() returns DeviceInfo
unlobito Jun 15, 2021
2e7c3bd
search for the Glow Display deviceTypeId
unlobito Jun 15, 2021
17562bb
call async_write_ha_state with a job
unlobito Sep 8, 2021
d4b9ae3
emit log messages for known setup failures
unlobito Sep 11, 2021
ae6ec9a
README: MQTT access
unlobito Oct 4, 2021
6bdf84c
Initial WIP potential fix for twos complement calculation of consumpt…
danstreeter Oct 14, 2021
ab265bd
Check hex length before dec conversion
unlobito Oct 26, 2021
e0ad1fe
introduce Glowdata for generic access from sensors
unlobito Dec 19, 2021
1ca3602
Determine sensor availability at runtime
unlobito Dec 21, 2021
845bc48
README.md: archived
unlobito Apr 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions custom_components/hildebrandglow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .glow import Glow
from .const import APP_ID, DOMAIN
from .glow import Glow, InvalidAuth

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)

Expand All @@ -23,7 +23,19 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hildebrand Glow from a config entry."""
glow = Glow(entry.data["app_id"], entry.data["token"])
glow = Glow(APP_ID, entry.data["username"], entry.data["password"])

try:
await hass.async_add_executor_job(glow.authenticate)
await hass.async_add_executor_job(glow.retrieve_cad_hardwareId)
await hass.async_add_executor_job(glow.connect_mqtt)

while not glow.broker_active:
continue

except InvalidAuth:
return False

hass.data[DOMAIN][entry.entry_id] = glow

for component in PLATFORMS:
Expand Down
7 changes: 3 additions & 4 deletions custom_components/hildebrandglow/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
import voluptuous as vol
from homeassistant import config_entries, core, data_entry_flow

from .const import DOMAIN # pylint:disable=unused-import
from .const import APP_ID, DOMAIN # pylint:disable=unused-import
from .glow import CannotConnect, Glow, InvalidAuth

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({"app_id": str, "username": str, "password": str})
DATA_SCHEMA = vol.Schema({"username": str, "password": str})


def config_object(data: dict, glow: Dict[str, Any]) -> Dict[str, Any]:
"""Prepare a ConfigEntity with authentication data and a temporary token."""
return {
"name": glow["name"],
"app_id": data["app_id"],
"username": data["username"],
"password": data["password"],
"token": glow["token"],
Expand All @@ -31,7 +30,7 @@ async def validate_input(hass: core.HomeAssistant, data: dict) -> Dict[str, Any]
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
glow = await hass.async_add_executor_job(
Glow.authenticate, data["app_id"], data["username"], data["password"]
Glow.authenticate, APP_ID, data["username"], data["password"]
)

# Return some info we want to store in the config entry.
Expand Down
1 change: 1 addition & 0 deletions custom_components/hildebrandglow/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Constants for the Hildebrand Glow integration."""

DOMAIN = "hildebrandglow"
APP_ID = "b0f1b774-a586-4f72-9edd-27ead8aa7a8d"
109 changes: 96 additions & 13 deletions custom_components/hildebrandglow/glow.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
"""Classes for interacting with the Glowmarkt API."""
from __future__ import annotations

from pprint import pprint
from typing import Any, Dict, List
from typing import TYPE_CHECKING, Any, Dict, List

import paho.mqtt.client as mqtt
import requests
from homeassistant import exceptions

from .mqttpayload import MQTTPayload

if TYPE_CHECKING:
from .sensor import GlowConsumptionCurrent


class Glow:
"""Bindings for the Hildebrand Glow Platform API."""

BASE_URL = "https://api.glowmarkt.com/api/v0-1"
HILDEBRAND_MQTT_HOST = "glowmqtt.energyhive.com"
HILDEBRAND_MQTT_TOPIC = "SMART/HILD/{hardwareId}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to make the MQTT topic configurable as well? Happy with HILD being the default, but for some reason I've got a legacy account and I need to use SMART/DCAD/{hardwareId}

Maybe name it mqtt_topic_prefix, that has a default value of HILD?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bje 👋 Thanks for raising this! That's an interesting edge case. I wonder if there's some way to automatically discover the right topic.

In the meantime, it seems there aren't any issues from subscribing to both topics (and only receiving messages from one), so I've put that in db42193.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw- I've opened a post on the official forums seeking advice on properly discovering the MQTT topic https://forum.glowmarkt.com/index.php?p=/discussion/75/mqtt-topic-variations


username: str
password: str

token: str

def __init__(self, app_id: str, token: str):
hardwareId: str
broker: mqtt.Client

sensors: Dict[str, GlowConsumptionCurrent] = {}

def __init__(self, app_id: str, username: str, password: str):
"""Create an authenticated Glow object."""
self.app_id = app_id
self.token = token
self.username = username
self.password = password

@classmethod
def authenticate(cls, app_id: str, username: str, password: str) -> Dict[str, Any]:
"""
Attempt to authenticate with Glowmarkt.
self.broker = mqtt.Client()
self.broker.username_pw_set(username=self.username, password=self.password)
self.broker.on_connect = self._cb_on_connect
self.broker.on_message = self._cb_on_message

Returns a time-limited access token.
"""
url = f"{cls.BASE_URL}/auth"
auth = {"username": username, "password": password}
headers = {"applicationId": app_id}
self.broker_active = False

def authenticate(self) -> None:
"""Attempt to authenticate with Glowmarkt."""
url = f"{self.BASE_URL}/auth"
auth = {"username": self.username, "password": self.password}
headers = {"applicationId": self.app_id}

try:
response = requests.post(url, json=auth, headers=headers)
Expand All @@ -35,11 +58,67 @@ def authenticate(cls, app_id: str, username: str, password: str) -> Dict[str, An
data = response.json()

if data["valid"]:
return data
self.token = data["token"]
else:
pprint(data)
raise InvalidAuth

def retrieve_devices(self) -> List[Dict[str, Any]]:
"""Retrieve the Zigbee devices known to Glowmarkt for the authenticated user."""
url = f"{self.BASE_URL}/device"
headers = {"applicationId": self.app_id, "token": self.token}

try:
response = requests.get(url, headers=headers)
except requests.Timeout:
raise CannotConnect

if response.status_code != 200:
raise InvalidAuth

data = response.json()
return data

def retrieve_cad_hardwareId(self) -> str:
"""Locate the Consumer Access Device's hardware ID from the devices list."""
ZIGBEE_GLOW_STICK = "1027b6e8-9bfd-4dcb-8068-c73f6413cfaf"

devices = self.retrieve_devices()

cad: Dict[str, Any] = next(
(dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), None
)

self.hardwareId = cad["hardwareId"]

return self.hardwareId

def connect_mqtt(self) -> None:
"""Connect the internal MQTT client to the discovered CAD."""
self.broker.connect(self.HILDEBRAND_MQTT_HOST)

self.broker.loop_start()

def _cb_on_connect(self, client, userdata, flags, rc):
"""Receive a CONNACK message from the server."""
client.subscribe(self.HILDEBRAND_MQTT_TOPIC.format(hardwareId=self.hardwareId))

self.broker_active = True

def _cb_on_disconnect(self, client, userdata, rc):
"""Receive notice the MQTT connection has disconnected."""
self.broker_active = False

def _cb_on_message(self, client, userdata, msg):
"""Receive a PUBLISH message from the server."""
payload = MQTTPayload(msg.payload)

if "electricity.consumption" in self.sensors:
self.sensors["electricity.consumption"].update_state(payload)

if "gas.consumption" in self.sensors:
self.sensors["gas.consumption"].update_state(payload)

def retrieve_resources(self) -> List[Dict[str, Any]]:
"""Retrieve the resources known to Glowmarkt for the authenticated user."""
url = f"{self.BASE_URL}/resource"
Expand Down Expand Up @@ -72,6 +151,10 @@ def current_usage(self, resource: Dict[str, Any]) -> Dict[str, Any]:
data = response.json()
return data

def register_sensor(self, sensor, resource):
"""Register a live sensor for dispatching MQTT messages."""
self.sensors[resource["classifier"]] = sensor


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
Expand Down
8 changes: 3 additions & 5 deletions custom_components/hildebrandglow/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
"name": "Hildebrand Glow",
"config_flow": true,
"documentation": "https://github.com/unlobito/ha-hildebrandglow",
"requirements": ["requests"],
"requirements": ["requests", "paho-mqtt"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@unlobito"
]
}
"codeowners": ["@unlobito"]
}
Loading