From e71d06e4bc3e24705ce493549cbce008a9c0f242 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Wed, 12 Aug 2020 21:13:42 +0100 Subject: [PATCH 01/26] Use the Glow App ID rather than prompting for one closes #15 --- custom_components/hildebrandglow/__init__.py | 4 ++-- custom_components/hildebrandglow/config_flow.py | 7 +++---- custom_components/hildebrandglow/const.py | 1 + custom_components/hildebrandglow/sensor.py | 9 +++------ custom_components/hildebrandglow/strings.json | 1 - custom_components/hildebrandglow/translations/en.json | 1 - 6 files changed, 9 insertions(+), 14 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 55c8ccf..3a8894c 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import APP_ID, DOMAIN from .glow import Glow CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -23,7 +23,7 @@ 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["token"]) hass.data[DOMAIN][entry.entry_id] = glow for component in PLATFORMS: diff --git a/custom_components/hildebrandglow/config_flow.py b/custom_components/hildebrandglow/config_flow.py index 8ecfd91..83638d4 100644 --- a/custom_components/hildebrandglow/config_flow.py +++ b/custom_components/hildebrandglow/config_flow.py @@ -5,19 +5,18 @@ import voluptuous as vol from homeassistant import config_entries, core -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"], @@ -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. diff --git a/custom_components/hildebrandglow/const.py b/custom_components/hildebrandglow/const.py index 9744e50..1d2b3e3 100644 --- a/custom_components/hildebrandglow/const.py +++ b/custom_components/hildebrandglow/const.py @@ -1,3 +1,4 @@ """Constants for the Hildebrand Glow integration.""" DOMAIN = "hildebrandglow" +APP_ID = "b0f1b774-a586-4f72-9edd-27ead8aa7a8d" diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 4b8859e..68d8117 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -7,7 +7,7 @@ from homeassistant.helpers.entity import Entity from .config_flow import config_object -from .const import DOMAIN +from .const import APP_ID, DOMAIN from .glow import Glow, InvalidAuth @@ -19,17 +19,14 @@ async def async_setup_entry( async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: glow_auth = await hass.async_add_executor_job( - Glow.authenticate, - config.data["app_id"], - config.data["username"], - config.data["password"], + Glow.authenticate, APP_ID, config.data["username"], config.data["password"], ) current_config = dict(config.data.copy()) new_config = config_object(current_config, glow_auth) hass.config_entries.async_update_entry(entry=config, data=new_config) - glow = Glow(config.data["app_id"], glow_auth["token"]) + glow = Glow(APP_ID, glow_auth["token"]) hass.data[DOMAIN][config.entry_id] = glow for entry in hass.data[DOMAIN]: diff --git a/custom_components/hildebrandglow/strings.json b/custom_components/hildebrandglow/strings.json index e02f59e..0674607 100644 --- a/custom_components/hildebrandglow/strings.json +++ b/custom_components/hildebrandglow/strings.json @@ -5,7 +5,6 @@ "user": { "title": "Hildebrand Glow API access", "data": { - "app_id": "Application ID", "username": "Username", "password": "Password" } diff --git a/custom_components/hildebrandglow/translations/en.json b/custom_components/hildebrandglow/translations/en.json index 545983b..109ff88 100644 --- a/custom_components/hildebrandglow/translations/en.json +++ b/custom_components/hildebrandglow/translations/en.json @@ -11,7 +11,6 @@ "step": { "user": { "data": { - "app_id": "Application ID", "password": "Password", "username": "Username" }, From bacb33a714ce6b162432ec92234e6e89ea2baf1a Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Wed, 12 Aug 2020 21:50:35 +0100 Subject: [PATCH 02/26] glow: add retrieve_devices method will be used to retrieve the CAD hardwareId for MQTT --- custom_components/hildebrandglow/glow.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 97f201d..4020897 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -40,6 +40,22 @@ def authenticate(cls, app_id: str, username: str, password: str) -> Dict[str, An 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_resources(self) -> List[Dict[str, Any]]: """Retrieve the resources known to Glowmarkt for the authenticated user.""" url = f"{self.BASE_URL}/resource" From b17515fe6126b35b9a110defbdf24dc954e406b1 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sun, 16 May 2021 22:47:22 +0100 Subject: [PATCH 03/26] move InvalidAuth handling out of sensor --- custom_components/hildebrandglow/__init__.py | 21 +++++++++++++++- custom_components/hildebrandglow/sensor.py | 25 +------------------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 3a8894c..de53414 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from .config_flow import config_object from .const import APP_ID, DOMAIN -from .glow import Glow +from .glow import Glow, InvalidAuth +from .sensor import GlowConsumptionCurrent CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -21,6 +23,23 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: return True +async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: + """Attempt to refresh the current Glow token.""" + glow_auth = await hass.async_add_executor_job( + Glow.authenticate, + APP_ID, + config.data["username"], + config.data["password"], + ) + + current_config = dict(config.data.copy()) + new_config = config_object(current_config, glow_auth) + hass.config_entries.async_update_entry(entry=config, data=new_config) + + glow = Glow(APP_ID, glow_auth["token"]) + hass.data[DOMAIN][config.entry_id] = glow + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hildebrand Glow from a config entry.""" glow = Glow(APP_ID, entry.data["token"]) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index bcec01f..832b49f 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -17,17 +17,7 @@ async def async_setup_entry( """Set up the sensor platform.""" new_entities = [] - async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: - glow_auth = await hass.async_add_executor_job( - Glow.authenticate, APP_ID, config.data["username"], config.data["password"], - ) - current_config = dict(config.data.copy()) - new_config = config_object(current_config, glow_auth) - hass.config_entries.async_update_entry(entry=config, data=new_config) - - glow = Glow(APP_ID, glow_auth["token"]) - hass.data[DOMAIN][config.entry_id] = glow for entry in hass.data[DOMAIN]: glow = hass.data[DOMAIN][entry] @@ -62,6 +52,7 @@ class GlowConsumptionCurrent(Entity): knownClassifiers = ["gas.consumption", "electricity.consumption"] available = True + should_poll = False def __init__(self, glow: Glow, resource: Dict[str, Any]): """Initialize the sensor.""" @@ -122,17 +113,3 @@ def unit_of_measurement(self) -> Optional[str]: return POWER_WATT else: return None - - async def async_update(self) -> None: - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ - try: - self._state = await self.hass.async_add_executor_job( - self.glow.current_usage, self.resource["resourceId"] - ) - except InvalidAuth: - # TODO: Trip the failed auth logic above somehow - self.available = False - pass From 24bf8d7d4b873c5887a4d9da044df07a35917c6a Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sun, 16 May 2021 22:50:19 +0100 Subject: [PATCH 04/26] calculate MQTT topic based on CAD ID --- custom_components/hildebrandglow/__init__.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index de53414..8186459 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -40,9 +40,35 @@ async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: hass.data[DOMAIN][config.entry_id] = glow +async def retrieve_cad_hardwareId(hass: HomeAssistant, glow: Glow) -> str: + """Locate the Consumer Access Device's hardware ID from the devices list.""" + ZIGBEE_GLOW_STICK = "1027b6e8-9bfd-4dcb-8068-c73f6413cfaf" + + devices = await hass.async_add_executor_job(glow.retrieve_devices) + + cad: Dict[str, Any] = next( + (dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), None + ) + + return cad["hardwareId"] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hildebrand Glow from a config entry.""" glow = Glow(APP_ID, entry.data["token"]) + + try: + hardwareId: str = await retrieve_cad_hardwareId(hass, glow) + mqtt_topic: str = f"SMART/HILD/{hardwareId}" + + resources = await hass.async_add_executor_job(glow.retrieve_resources) + + except InvalidAuth: + try: + await handle_failed_auth(entry, hass) + return False + except InvalidAuth: + return False + hass.data[DOMAIN][entry.entry_id] = glow for component in PLATFORMS: From 9f9c2f6b59b119fbc814901b844209fa5dcfd53b Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sun, 16 May 2021 23:51:22 +0100 Subject: [PATCH 05/26] connect to MQTT server with discovered CAD ID --- custom_components/hildebrandglow/__init__.py | 19 ++------ custom_components/hildebrandglow/glow.py | 46 +++++++++++++++++++ .../hildebrandglow/manifest.json | 8 ++-- requirements-dev.txt | 1 + 4 files changed, 53 insertions(+), 21 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 8186459..8dbfa33 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -40,27 +40,14 @@ async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: hass.data[DOMAIN][config.entry_id] = glow -async def retrieve_cad_hardwareId(hass: HomeAssistant, glow: Glow) -> str: - """Locate the Consumer Access Device's hardware ID from the devices list.""" - ZIGBEE_GLOW_STICK = "1027b6e8-9bfd-4dcb-8068-c73f6413cfaf" - - devices = await hass.async_add_executor_job(glow.retrieve_devices) - - cad: Dict[str, Any] = next( - (dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), None - ) - - return cad["hardwareId"] - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hildebrand Glow from a config entry.""" glow = Glow(APP_ID, entry.data["token"]) try: - hardwareId: str = await retrieve_cad_hardwareId(hass, glow) - mqtt_topic: str = f"SMART/HILD/{hardwareId}" - - resources = await hass.async_add_executor_job(glow.retrieve_resources) + await hass.async_add_executor_job(glow.retrieve_cad_hardwareId) + resources = await hass.async_create_task(glow.connect_mqtt()) + hass.async_create_task(glow.retrieve_mqtt()) except InvalidAuth: try: diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 4020897..9b60e48 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -5,12 +5,18 @@ import requests from homeassistant import exceptions +from hbmqtt.client import MQTTClient +from hbmqtt.mqtt.constants import QOS_1 + class Glow: """Bindings for the Hildebrand Glow Platform API.""" BASE_URL = "https://api.glowmarkt.com/api/v0-1" + hardwareId: str + broker: MQTTClient + def __init__(self, app_id: str, token: str): """Create an authenticated Glow object.""" self.app_id = app_id @@ -56,6 +62,46 @@ def retrieve_devices(self) -> List[Dict[str, Any]]: 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 + + async def connect_mqtt(self) -> None: + """Connect the internal MQTT client to the discovered CAD""" + HILDEBRAND_MQTT_HOST = ( + "mqtts://USER:PASS@glowmqtt.energyhive.com/" + ) + HILDEBRAND_MQTT_TOPIC = "SMART/HILD/{hardwareId}" + + cad_hwId = self.hardwareId + topic = HILDEBRAND_MQTT_TOPIC.format(hardwareId=cad_hwId) + + self.broker = MQTTClient() + + await self.broker.connect(HILDEBRAND_MQTT_HOST) + + await self.broker.subscribe( + [ + (topic, QOS_1), + ] + ) + + async def retrieve_mqtt(self) -> None: + while True: + message = await self.broker.deliver_message() + packet = message.publish_packet.payload.data.decode() + print(packet) + def retrieve_resources(self) -> List[Dict[str, Any]]: """Retrieve the resources known to Glowmarkt for the authenticated user.""" url = f"{self.BASE_URL}/resource" diff --git a/custom_components/hildebrandglow/manifest.json b/custom_components/hildebrandglow/manifest.json index 81cd053..97f63ea 100644 --- a/custom_components/hildebrandglow/manifest.json +++ b/custom_components/hildebrandglow/manifest.json @@ -3,12 +3,10 @@ "name": "Hildebrand Glow", "config_flow": true, "documentation": "https://github.com/unlobito/ha-hildebrandglow", - "requirements": ["requests"], + "requirements": ["requests", "amqtt"], "ssdp": [], "zeroconf": [], "homekit": {}, "dependencies": [], - "codeowners": [ - "@unlobito" - ] -} \ No newline at end of file + "codeowners": ["@unlobito"] +} diff --git a/requirements-dev.txt b/requirements-dev.txt index 6470ad2..b9e8b33 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,3 +2,4 @@ black==21.4b0 flake8==3.9.1 isort==5.8.0 mypy==0.812 +amqtt==0.10.0a3 From dd104d8d94567fde1b520016bc236c56de7ec42d Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 02:17:31 +0100 Subject: [PATCH 06/26] persist credentials in Glow object --- custom_components/hildebrandglow/__init__.py | 30 +++-------------- custom_components/hildebrandglow/glow.py | 35 ++++++++++---------- custom_components/hildebrandglow/sensor.py | 12 ++----- 3 files changed, 24 insertions(+), 53 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 8dbfa33..8934c8e 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -6,10 +6,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .config_flow import config_object from .const import APP_ID, DOMAIN from .glow import Glow, InvalidAuth -from .sensor import GlowConsumptionCurrent CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -23,38 +21,18 @@ async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: return True -async def handle_failed_auth(config: ConfigEntry, hass: HomeAssistant) -> None: - """Attempt to refresh the current Glow token.""" - glow_auth = await hass.async_add_executor_job( - Glow.authenticate, - APP_ID, - config.data["username"], - config.data["password"], - ) - - current_config = dict(config.data.copy()) - new_config = config_object(current_config, glow_auth) - hass.config_entries.async_update_entry(entry=config, data=new_config) - - glow = Glow(APP_ID, glow_auth["token"]) - hass.data[DOMAIN][config.entry_id] = glow - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hildebrand Glow from a config entry.""" - glow = Glow(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) - resources = await hass.async_create_task(glow.connect_mqtt()) + await hass.async_create_task(glow.connect_mqtt()) hass.async_create_task(glow.retrieve_mqtt()) except InvalidAuth: - try: - await handle_failed_auth(entry, hass) - return False - except InvalidAuth: - return False + return False hass.data[DOMAIN][entry.entry_id] = glow diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 9b60e48..883d90b 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -3,10 +3,9 @@ from typing import Any, Dict, List import requests -from homeassistant import exceptions - from hbmqtt.client import MQTTClient from hbmqtt.mqtt.constants import QOS_1 +from homeassistant import exceptions class Glow: @@ -14,24 +13,29 @@ class Glow: BASE_URL = "https://api.glowmarkt.com/api/v0-1" + username: str + password: str + + token: str + hardwareId: str broker: MQTTClient - def __init__(self, app_id: str, token: str): + 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]: + def authenticate(self) -> None: """ Attempt to authenticate with Glowmarkt. Returns a time-limited access token. """ - url = f"{cls.BASE_URL}/auth" - auth = {"username": username, "password": password} - headers = {"applicationId": app_id} + 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) @@ -41,7 +45,7 @@ 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 @@ -77,14 +81,11 @@ def retrieve_cad_hardwareId(self) -> str: return self.hardwareId async def connect_mqtt(self) -> None: - """Connect the internal MQTT client to the discovered CAD""" + """Connect the internal MQTT client to the discovered CAD.""" HILDEBRAND_MQTT_HOST = ( - "mqtts://USER:PASS@glowmqtt.energyhive.com/" + f"mqtts://{self.username}:{self.password}@glowmqtt.energyhive.com/" ) - HILDEBRAND_MQTT_TOPIC = "SMART/HILD/{hardwareId}" - - cad_hwId = self.hardwareId - topic = HILDEBRAND_MQTT_TOPIC.format(hardwareId=cad_hwId) + HILDEBRAND_MQTT_TOPIC = f"SMART/HILD/{self.hardwareId}" self.broker = MQTTClient() @@ -92,7 +93,7 @@ async def connect_mqtt(self) -> None: await self.broker.subscribe( [ - (topic, QOS_1), + (HILDEBRAND_MQTT_TOPIC, QOS_1), ] ) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 832b49f..8ae8db5 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -6,8 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from .config_flow import config_object -from .const import APP_ID, DOMAIN +from .const import DOMAIN from .glow import Glow, InvalidAuth @@ -17,8 +16,6 @@ async def async_setup_entry( """Set up the sensor platform.""" new_entities = [] - - for entry in hass.data[DOMAIN]: glow = hass.data[DOMAIN][entry] @@ -27,13 +24,8 @@ async def async_setup_entry( try: resources = await hass.async_add_executor_job(glow.retrieve_resources) except InvalidAuth: - try: - await handle_failed_auth(config, hass) - except InvalidAuth: - return False + return False - glow = hass.data[DOMAIN][entry] - resources = await hass.async_add_executor_job(glow.retrieve_resources) for resource in resources: if resource["classifier"] in GlowConsumptionCurrent.knownClassifiers: sensor = GlowConsumptionCurrent(glow, resource) From 3e7432a79a5c17df6db9a30dabb515d608544f0d Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 03:30:19 +0100 Subject: [PATCH 07/26] WIP: route MQTT data to sensor object --- custom_components/hildebrandglow/glow.py | 20 +++++++- .../hildebrandglow/mqttpayload.py | 49 +++++++++++++++++++ custom_components/hildebrandglow/sensor.py | 13 +++-- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 custom_components/hildebrandglow/mqttpayload.py diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 883d90b..eb72aca 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -1,12 +1,19 @@ +from __future__ import annotations + """Classes for interacting with the Glowmarkt API.""" from pprint import pprint -from typing import Any, Dict, List +from typing import TYPE_CHECKING, Any, Dict, List import requests from hbmqtt.client import MQTTClient from hbmqtt.mqtt.constants import QOS_1 from homeassistant import exceptions +from .mqttpayload import MQTTPayload + +if TYPE_CHECKING: + from .sensor import GlowConsumptionCurrent + class Glow: """Bindings for the Hildebrand Glow Platform API.""" @@ -21,6 +28,8 @@ class Glow: hardwareId: str broker: MQTTClient + sensors: Dict[str, GlowConsumptionCurrent] = dict() + def __init__(self, app_id: str, username: str, password: str): """Create an authenticated Glow object.""" self.app_id = app_id @@ -101,7 +110,11 @@ async def retrieve_mqtt(self) -> None: while True: message = await self.broker.deliver_message() packet = message.publish_packet.payload.data.decode() - print(packet) + + payload = MQTTPayload(packet) + + if "electricity.consumption" in self.sensors: + self.sensors["electricity.consumption"].update_state(payload) def retrieve_resources(self) -> List[Dict[str, Any]]: """Retrieve the resources known to Glowmarkt for the authenticated user.""" @@ -135,6 +148,9 @@ def current_usage(self, resource: Dict[str, Any]) -> Dict[str, Any]: data = response.json() return data + def register_sensor(self, sensor, resource): + self.sensors[resource["classifier"]] = sensor + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py new file mode 100644 index 0000000..61a354e --- /dev/null +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -0,0 +1,49 @@ +import json +from typing import Any, Dict + + +class Meter: + def __init__(self, payload: Dict[str, Any]): + historical_consumption = ( + payload["0702"]["04"] if "04" in payload["0702"] else {} + ) + + self.consumption = ( + int(historical_consumption["00"], 16) + if "00" in historical_consumption + else None + ) + self.daily_consumption = ( + int(historical_consumption["01"], 16) + if "01" in historical_consumption + else None + ) + self.weekly_consumption = ( + int(historical_consumption["30"], 16) + if "30" in historical_consumption + else None + ) + self.monthly_consumption = ( + int(historical_consumption["40"], 16) + if "40" in historical_consumption + else None + ) + + formatting = payload["0702"]["03"] if "03" in payload["0702"] else {} + self.multiplier = int(formatting["01"], 16) if "01" in formatting else None + self.divisor = int(formatting["02"], 16) if "02" in formatting else None + + self.meter = ( + int(payload["0702"]["00"]["00"], 16) + if "00" in payload["0702"]["00"] + else None + ) + + +class MQTTPayload: + payload: Dict[str, Any] + + def __init__(self, payload: str): + self.payload = json.loads(payload) + self.electricity = Meter(self.payload["elecMtr"]) + self.gas = Meter(self.payload["gasMtr"]) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 8ae8db5..155dacd 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -8,6 +8,7 @@ from .const import DOMAIN from .glow import Glow, InvalidAuth +from .mqttpayload import Meter async def async_setup_entry( @@ -29,6 +30,7 @@ async def async_setup_entry( for resource in resources: if resource["classifier"] in GlowConsumptionCurrent.knownClassifiers: sensor = GlowConsumptionCurrent(glow, resource) + glow.register_sensor(sensor, resource) new_entities.append(sensor) async_add_entities(new_entities) @@ -48,7 +50,7 @@ class GlowConsumptionCurrent(Entity): def __init__(self, glow: Glow, resource: Dict[str, Any]): """Initialize the sensor.""" - self._state: Optional[Dict[str, Any]] = None + self._state: Optional[Meter] = None self.glow = glow self.resource = resource @@ -89,10 +91,15 @@ def device_info(self) -> Optional[Dict[str, Any]]: def state(self) -> Optional[str]: """Return the state of the sensor.""" if self._state: - return self._state["data"][0][1] + return self._state.consumption else: return None + def update_state(self, meter) -> None: + """Receive an MQTT update from Glow and update the internal state.""" + self._state = meter.electricity + self.async_write_ha_state() + @property def device_class(self) -> str: """Return the device class (always DEVICE_CLASS_POWER).""" @@ -101,7 +108,7 @@ def device_class(self) -> str: @property def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement.""" - if self._state is not None and self._state["units"] == "W": + if self._state is not None: return POWER_WATT else: return None From 84939dd5a68c53ead2fca9544d78f4f7ede21580 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 14:05:03 +0100 Subject: [PATCH 08/26] migrate to paho-mqtt --- custom_components/hildebrandglow/__init__.py | 6 +- custom_components/hildebrandglow/glow.py | 61 ++++++++++--------- .../hildebrandglow/manifest.json | 2 +- requirements-dev.txt | 2 +- 4 files changed, 38 insertions(+), 33 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 8934c8e..4428dde 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -28,8 +28,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await hass.async_add_executor_job(glow.authenticate) await hass.async_add_executor_job(glow.retrieve_cad_hardwareId) - await hass.async_create_task(glow.connect_mqtt()) - hass.async_create_task(glow.retrieve_mqtt()) + await hass.async_add_executor_job(glow.connect_mqtt) + + while not glow.broker_active: + continue except InvalidAuth: return False diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index eb72aca..db89f16 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -1,12 +1,11 @@ +"""Classes for interacting with the Glowmarkt API.""" from __future__ import annotations -"""Classes for interacting with the Glowmarkt API.""" from pprint import pprint from typing import TYPE_CHECKING, Any, Dict, List +import paho.mqtt.client as mqtt import requests -from hbmqtt.client import MQTTClient -from hbmqtt.mqtt.constants import QOS_1 from homeassistant import exceptions from .mqttpayload import MQTTPayload @@ -19,6 +18,8 @@ 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}" username: str password: str @@ -26,9 +27,9 @@ class Glow: token: str hardwareId: str - broker: MQTTClient + broker: mqtt - sensors: Dict[str, GlowConsumptionCurrent] = dict() + sensors: Dict[str, GlowConsumptionCurrent] = {} def __init__(self, app_id: str, username: str, password: str): """Create an authenticated Glow object.""" @@ -36,12 +37,15 @@ def __init__(self, app_id: str, username: str, password: str): self.username = username self.password = password - def authenticate(self) -> None: - """ - 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 + + self.broker_active = False - Returns a time-limited access token. - """ + 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} @@ -89,32 +93,30 @@ def retrieve_cad_hardwareId(self) -> str: return self.hardwareId - async def connect_mqtt(self) -> None: + def connect_mqtt(self) -> None: """Connect the internal MQTT client to the discovered CAD.""" - HILDEBRAND_MQTT_HOST = ( - f"mqtts://{self.username}:{self.password}@glowmqtt.energyhive.com/" - ) - HILDEBRAND_MQTT_TOPIC = f"SMART/HILD/{self.hardwareId}" + self.broker.connect(self.HILDEBRAND_MQTT_HOST) - self.broker = MQTTClient() + self.broker.loop_start() - await self.broker.connect(HILDEBRAND_MQTT_HOST) + 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)) - await self.broker.subscribe( - [ - (HILDEBRAND_MQTT_TOPIC, QOS_1), - ] - ) + self.broker_active = True + + def _cb_on_disconnect(self, client, userdata, rc): + """Receive notice the MQTT connection has disconnected.""" + self.broker_active = False - async def retrieve_mqtt(self) -> None: - while True: - message = await self.broker.deliver_message() - packet = message.publish_packet.payload.data.decode() + def _cb_on_message(self, client, userdata, msg): + """Receive a PUBLISH message from the server.""" + print(msg.topic + " " + str(msg.payload)) - payload = MQTTPayload(packet) + payload = MQTTPayload(msg.payload) - if "electricity.consumption" in self.sensors: - self.sensors["electricity.consumption"].update_state(payload) + if "electricity.consumption" in self.sensors: + self.sensors["electricity.consumption"].update_state(payload) def retrieve_resources(self) -> List[Dict[str, Any]]: """Retrieve the resources known to Glowmarkt for the authenticated user.""" @@ -149,6 +151,7 @@ def current_usage(self, resource: Dict[str, Any]) -> Dict[str, Any]: return data def register_sensor(self, sensor, resource): + """Register a live sensor for dispatching MQTT messages.""" self.sensors[resource["classifier"]] = sensor diff --git a/custom_components/hildebrandglow/manifest.json b/custom_components/hildebrandglow/manifest.json index 97f63ea..8914feb 100644 --- a/custom_components/hildebrandglow/manifest.json +++ b/custom_components/hildebrandglow/manifest.json @@ -3,7 +3,7 @@ "name": "Hildebrand Glow", "config_flow": true, "documentation": "https://github.com/unlobito/ha-hildebrandglow", - "requirements": ["requests", "amqtt"], + "requirements": ["requests", "paho-mqtt"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements-dev.txt b/requirements-dev.txt index b9e8b33..f2e682a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ black==21.4b0 flake8==3.9.1 isort==5.8.0 mypy==0.812 -amqtt==0.10.0a3 +paho-mqtt==1.5.1 From 61408de8169f574ceba6efa25d1348e5788870a2 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 20:39:41 +0100 Subject: [PATCH 09/26] parse more MQTTPayload data --- custom_components/hildebrandglow/glow.py | 4 +- .../hildebrandglow/mqttpayload.py | 189 +++++++++++++++--- custom_components/hildebrandglow/sensor.py | 5 +- 3 files changed, 162 insertions(+), 36 deletions(-) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index db89f16..80e1fd3 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -27,7 +27,7 @@ class Glow: token: str hardwareId: str - broker: mqtt + broker: mqtt.Client sensors: Dict[str, GlowConsumptionCurrent] = {} @@ -111,8 +111,6 @@ def _cb_on_disconnect(self, client, userdata, rc): def _cb_on_message(self, client, userdata, msg): """Receive a PUBLISH message from the server.""" - print(msg.topic + " " + str(msg.payload)) - payload = MQTTPayload(msg.payload) if "electricity.consumption" in self.sensors: diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index 61a354e..f60e48b 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -1,37 +1,159 @@ import json +from enum import Enum from typing import Any, Dict class Meter: - def __init__(self, payload: Dict[str, Any]): - historical_consumption = ( - payload["0702"]["04"] if "04" in payload["0702"] else {} - ) + class ReadingInformationSet: + current_summation_delivered: int + current_summation_received: int + current_max_demand_delivered: int + reading_snapshot_time: str + supply_status: int - self.consumption = ( - int(historical_consumption["00"], 16) - if "00" in historical_consumption - else None - ) - self.daily_consumption = ( - int(historical_consumption["01"], 16) - if "01" in historical_consumption - else None - ) - self.weekly_consumption = ( - int(historical_consumption["30"], 16) - if "30" in historical_consumption - else None - ) - self.monthly_consumption = ( - int(historical_consumption["40"], 16) - if "40" in historical_consumption - else None - ) + def __init__(self, payload: Dict[str, Any]): + reading_information_set = ( + payload["0702"]["00"] if "00" in payload["0702"] else {} + ) + + self.current_summation_delivered = ( + int(reading_information_set["00"], 16) + if "00" in reading_information_set + else None + ) + self.current_summation_received = ( + int(reading_information_set["01"], 16) + if "01" in reading_information_set + else None + ) + self.current_max_demand_delivered = ( + int(reading_information_set["02"], 16) + if "02" in reading_information_set + else None + ) + self.reading_snapshot_time = ( + int(reading_information_set["07"], 16) + if "07" in reading_information_set + else None + ) + self.supply_status = ( + int(reading_information_set["07"], 16) + if "07" in reading_information_set + else None + ) + + class MeterStatus: + status: str + + def __init__(self, payload: Dict[str, Any]): + meter_status = payload["0702"]["02"] if "02" in payload["0702"] else {} + + self.status = meter_status.get("00") + + class Formatting: + class UnitofMeasure(Enum): + KWH = "00" + M3 = "01" - formatting = payload["0702"]["03"] if "03" in payload["0702"] else {} - self.multiplier = int(formatting["01"], 16) if "01" in formatting else None - self.divisor = int(formatting["02"], 16) if "02" in formatting else None + class MeteringDeviceType(Enum): + ELECTRIC = "00" + GAS = "80" + + unit_of_measure: UnitofMeasure + multiplier: int + divisor: int + summation_formatting: str + demand_formatting: str + metering_device_type: MeteringDeviceType + siteID: str + meter_serial_number: str + alternative_unit_of_measure: UnitofMeasure + + def __init__(self, payload: Dict[str, Any]): + formatting = payload["0702"]["03"] if "03" in payload["0702"] else {} + + self.unit_of_measure = self.UnitofMeasure(formatting.get("00", "00")) + self.multiplier = int(formatting["01"], 16) if "01" in formatting else None + self.divisor = int(formatting["02"], 16) if "02" in formatting else None + self.summation_formatting = formatting.get("03") + self.demand_formatting = formatting.get("04") + self.metering_device_type = ( + self.MeteringDeviceType(formatting["06"]) + if "06" in formatting + else None + ) + self.siteID = formatting.get("07") + self.meter_serial_number = formatting.get("08") + self.alternative_unit_of_measure = ( + self.UnitofMeasure(formatting["12"]) if "12" in formatting else None + ) + + class HistoricalConsumption: + instantaneous_demand: int + current_day_consumption_delivered: int + current_week_consumption_delivered: int + current_month_consumption_delivered: int + + def __init__(self, payload: Dict[str, Any]): + historical_consumption = ( + payload["0702"]["04"] if "04" in payload["0702"] else {} + ) + + self.instantaneous_demand = ( + int(historical_consumption["00"], 16) + if "00" in historical_consumption + else None + ) + self.current_day_consumption_delivered = ( + int(historical_consumption["01"], 16) + if "01" in historical_consumption + else None + ) + self.current_week_consumption_delivered = ( + int(historical_consumption["30"], 16) + if "30" in historical_consumption + else None + ) + self.current_week_consumption_delivered = ( + int(historical_consumption["40"], 16) + if "40" in historical_consumption + else None + ) + + class AlternativeHistoricalConsumption: + current_day_consumption_delivered: int + current_week_consumption_delivered: int + current_month_consumption_delivered: int + + def __init__(self, payload: Dict[str, Any]): + alternative_historical_consumption = ( + payload["0702"]["0C"] if "0C" in payload["0702"] else {} + ) + + self.current_day_consumption_delivered = ( + int(alternative_historical_consumption["01"], 16) + if "01" in alternative_historical_consumption + else None + ) + self.current_week_consumption_delivered = ( + int(alternative_historical_consumption["30"], 16) + if "30" in alternative_historical_consumption + else None + ) + self.current_week_consumption_delivered = ( + int(alternative_historical_consumption["40"], 16) + if "40" in alternative_historical_consumption + else None + ) + + def __init__(self, payload: Dict[str, Any]): + self.reading_information_set = self.ReadingInformationSet(payload) + self.meter_status = self.MeterStatus(payload) + self.formatting = self.Formatting(payload) + self.historical_consumption = self.HistoricalConsumption(payload) + self.alternative_historical_consumption = self.AlternativeHistoricalConsumption( + payload + ) self.meter = ( int(payload["0702"]["00"]["00"], 16) @@ -41,9 +163,14 @@ def __init__(self, payload: Dict[str, Any]): class MQTTPayload: - payload: Dict[str, Any] + electricity: Meter + gas: Meter def __init__(self, payload: str): - self.payload = json.loads(payload) - self.electricity = Meter(self.payload["elecMtr"]) - self.gas = Meter(self.payload["gasMtr"]) + payload = json.loads(payload) + self.electricity = ( + Meter(payload["elecMtr"]) if "03" in payload["elecMtr"]["0702"] else None + ) + self.gas = ( + Meter(payload["gasMtr"]) if "03" in payload["gasMtr"]["0702"] else None + ) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 155dacd..63ae6eb 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -45,12 +45,13 @@ class GlowConsumptionCurrent(Entity): knownClassifiers = ["gas.consumption", "electricity.consumption"] + _state: Optional[Meter] available = True should_poll = False def __init__(self, glow: Glow, resource: Dict[str, Any]): """Initialize the sensor.""" - self._state: Optional[Meter] = None + self._state = None self.glow = glow self.resource = resource @@ -91,7 +92,7 @@ def device_info(self) -> Optional[Dict[str, Any]]: def state(self) -> Optional[str]: """Return the state of the sensor.""" if self._state: - return self._state.consumption + return self._state.historical_consumption.instantaneous_demand else: return None From 610f5de8eaf5a02b770599ac4c126f03b50421ee Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 21:10:35 +0100 Subject: [PATCH 10/26] MQTTPayload: docstrings --- .../hildebrandglow/mqttpayload.py | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index f60e48b..69ffc21 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -1,17 +1,39 @@ +"""Helper classes for Zigbee Smart Energy Profile data.""" import json from enum import Enum from typing import Any, Dict class Meter: + """Information received regarding a single smart meter.""" + class ReadingInformationSet: + """Attributes providing remote access to meter readings.""" + + class SupplyStatus(Enum): + """Meter supply states.""" + + OFF = "00" + ARMED = "01" + ON = "02" + current_summation_delivered: int + """Import energy usage""" + current_summation_received: int + """Export energy usage""" + current_max_demand_delivered: int + """Maximum import energy usage rate""" + reading_snapshot_time: str - supply_status: int + """Last time all of the reported attributed were updated""" + + supply_status: SupplyStatus + """Current state of the meter's supply.""" def __init__(self, payload: Dict[str, Any]): + """Parse meter readings from the received payload.""" reading_information_set = ( payload["0702"]["00"] if "00" in payload["0702"] else {} ) @@ -36,40 +58,66 @@ def __init__(self, payload: Dict[str, Any]): if "07" in reading_information_set else None ) - self.supply_status = ( - int(reading_information_set["07"], 16) - if "07" in reading_information_set - else None + self.supply_status = self.SupplyStatus( + reading_information_set.get("07", "00") ) class MeterStatus: + """Information about the meter's error conditions.""" + status: str + """Meter error conditions""" def __init__(self, payload: Dict[str, Any]): + """Parse meter error conditions from the received payload.""" meter_status = payload["0702"]["02"] if "02" in payload["0702"] else {} self.status = meter_status.get("00") class Formatting: + """Information about the format used for metering data.""" + class UnitofMeasure(Enum): + """Units of Measurement.""" + KWH = "00" M3 = "01" class MeteringDeviceType(Enum): + """Metering Device Types.""" + ELECTRIC = "00" GAS = "80" unit_of_measure: UnitofMeasure + """Unit for the measured value.""" + multiplier: int + """Multiplier value for smart meter readings.""" + divisor: int + """Divisor value for smart meter readings.""" + summation_formatting: str + """Bitmap representing decimal places in Summation readings.""" + demand_formatting: str + """Bitmap representing decimal places in Demand readings.""" + metering_device_type: MeteringDeviceType + """Smart meter device type.""" + siteID: str + """Electricicity MPAN / Gas MPRN.""" + meter_serial_number: str + """Smart meter serial number.""" + alternative_unit_of_measure: UnitofMeasure + """Alternative unit for the measured value.""" def __init__(self, payload: Dict[str, Any]): + """Parse formatting data from the received payload.""" formatting = payload["0702"]["03"] if "03" in payload["0702"] else {} self.unit_of_measure = self.UnitofMeasure(formatting.get("00", "00")) @@ -89,12 +137,22 @@ def __init__(self, payload: Dict[str, Any]): ) class HistoricalConsumption: + """Information about the meter's historical readings.""" + instantaneous_demand: int + """Instantaneous import energy usage rate""" + current_day_consumption_delivered: int + """Import energy used in the current day.""" + current_week_consumption_delivered: int + """Import energy used in the current week.""" + current_month_consumption_delivered: int + """Import energy used in the current month.""" def __init__(self, payload: Dict[str, Any]): + """Parse historical meter readings from the received payload.""" historical_consumption = ( payload["0702"]["04"] if "04" in payload["0702"] else {} ) @@ -121,11 +179,19 @@ def __init__(self, payload: Dict[str, Any]): ) class AlternativeHistoricalConsumption: + """Information about the meter's altenative historical readings.""" + current_day_consumption_delivered: int + """Import energy used in the current day.""" + current_week_consumption_delivered: int + """Import energy used in the current week.""" + current_month_consumption_delivered: int + """Import energy used in the current month.""" def __init__(self, payload: Dict[str, Any]): + """Parse alternative historical meter readings from the received payload.""" alternative_historical_consumption = ( payload["0702"]["0C"] if "0C" in payload["0702"] else {} ) @@ -147,6 +213,7 @@ def __init__(self, payload: Dict[str, Any]): ) def __init__(self, payload: Dict[str, Any]): + """Parse meter data from the received payload using helper classes.""" self.reading_information_set = self.ReadingInformationSet(payload) self.meter_status = self.MeterStatus(payload) self.formatting = self.Formatting(payload) @@ -163,10 +230,16 @@ def __init__(self, payload: Dict[str, Any]): class MQTTPayload: + """Object representing a payload received over MQTT.""" + electricity: Meter + """Data interpreted from an electricity meter.""" + gas: Meter + """Data interpreted from a gas meter.""" def __init__(self, payload: str): + """Create internal Meter instances based off the unprocessed payload.""" payload = json.loads(payload) self.electricity = ( Meter(payload["elecMtr"]) if "03" in payload["elecMtr"]["0702"] else None From 71af3ba98daa2d1b354f72abd5d7dc468f74f7f5 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 21:21:03 +0100 Subject: [PATCH 11/26] attempt gas readings --- custom_components/hildebrandglow/glow.py | 3 +++ custom_components/hildebrandglow/sensor.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 80e1fd3..91ea8c6 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -116,6 +116,9 @@ def _cb_on_message(self, client, userdata, msg): 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" diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 63ae6eb..b493e63 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -2,7 +2,7 @@ from typing import Any, Callable, Dict, Optional from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_POWER, POWER_WATT +from homeassistant.const import DEVICE_CLASS_POWER, POWER_WATT, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity @@ -92,7 +92,11 @@ def device_info(self) -> Optional[Dict[str, Any]]: def state(self) -> Optional[str]: """Return the state of the sensor.""" if self._state: - return self._state.historical_consumption.instantaneous_demand + if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": + return self._state.historical_consumption.instantaneous_demand + elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": + alt = self._state.alternative_historical_consumption + return alt.current_day_consumption_delivered else: return None @@ -110,6 +114,9 @@ def device_class(self) -> str: def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement.""" if self._state is not None: - return POWER_WATT + if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": + return POWER_WATT + elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": + return VOLUME_CUBIC_METERS else: return None From 68d34e7acd746b5a30054ecc254fa5dcf81ddde6 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 21:46:28 +0100 Subject: [PATCH 12/26] typo in supply_status key --- custom_components/hildebrandglow/mqttpayload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index 69ffc21..70bd4a4 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -59,7 +59,7 @@ def __init__(self, payload: Dict[str, Any]): else None ) self.supply_status = self.SupplyStatus( - reading_information_set.get("07", "00") + reading_information_set.get("14", "00") ) class MeterStatus: From db42193d8eb235d029b4152aed16597f1cb6a4bc Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 21:53:14 +0100 Subject: [PATCH 13/26] subscribe to HILD and DCAD topics --- custom_components/hildebrandglow/glow.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 91ea8c6..4dc282c 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -20,6 +20,7 @@ class Glow: BASE_URL = "https://api.glowmarkt.com/api/v0-1" HILDEBRAND_MQTT_HOST = "glowmqtt.energyhive.com" HILDEBRAND_MQTT_TOPIC = "SMART/HILD/{hardwareId}" + HILDEBRAND_MQTT_LEGACY = "SMART/DCAD/{hardwareId}" username: str password: str @@ -101,7 +102,15 @@ def connect_mqtt(self) -> None: 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)) + client.subscribe( + [ + (self.HILDEBRAND_MQTT_TOPIC.format(hardwareId=self.hardwareId), 0), + ( + self.HILDEBRAND_MQTT_LEGACY.format(hardwareId=self.hardwareId), + 0, + ), + ] + ) self.broker_active = True From 6bdd3990fe8c5a56bf4545e486a38418c5c4aeca Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 17 May 2021 22:29:18 +0100 Subject: [PATCH 14/26] address mypy errors --- .../hildebrandglow/config_flow.py | 14 ++--- custom_components/hildebrandglow/glow.py | 19 ++++--- .../hildebrandglow/mqttpayload.py | 54 +++++++++---------- custom_components/hildebrandglow/sensor.py | 13 +++-- setup.cfg | 3 ++ 5 files changed, 56 insertions(+), 47 deletions(-) diff --git a/custom_components/hildebrandglow/config_flow.py b/custom_components/hildebrandglow/config_flow.py index 05110f1..4d7be9a 100644 --- a/custom_components/hildebrandglow/config_flow.py +++ b/custom_components/hildebrandglow/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Hildebrand Glow integration.""" +from __future__ import annotations + import logging from typing import Any, Dict @@ -19,8 +21,6 @@ def config_object(data: dict, glow: Dict[str, Any]) -> Dict[str, Any]: "name": glow["name"], "username": data["username"], "password": data["password"], - "token": glow["token"], - "token_exp": glow["exp"], } @@ -29,12 +29,12 @@ 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, APP_ID, data["username"], data["password"] - ) + glow = Glow(APP_ID, data["username"], data["password"]) + + auth_data: Dict[str, Any] = await hass.async_add_executor_job(glow.authenticate) # Return some info we want to store in the config entry. - return config_object(data, glow) + return config_object(data, auth_data) class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -44,7 +44,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONNECTION_CLASS = config_entries.SOURCE_USER async def async_step_user( - self, user_input: Dict = None + self, user_input: dict[str, Any] | None = None ) -> data_entry_flow.FlowResult: """Handle the initial step.""" errors = {} diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 4dc282c..eda0785 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -45,7 +45,7 @@ def __init__(self, app_id: str, username: str, password: str): self.broker_active = False - def authenticate(self) -> None: + def authenticate(self) -> Dict[str, Any]: """Attempt to authenticate with Glowmarkt.""" url = f"{self.BASE_URL}/auth" auth = {"username": self.username, "password": self.password} @@ -60,6 +60,7 @@ def authenticate(self) -> None: if data["valid"]: self.token = data["token"] + return data else: pprint(data) raise InvalidAuth @@ -87,7 +88,7 @@ def retrieve_cad_hardwareId(self) -> str: devices = self.retrieve_devices() cad: Dict[str, Any] = next( - (dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), None + (dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), {} ) self.hardwareId = cad["hardwareId"] @@ -100,7 +101,9 @@ def connect_mqtt(self) -> None: self.broker.loop_start() - def _cb_on_connect(self, client, userdata, flags, rc): + def _cb_on_connect( + self, client: mqtt, userdata: Any, flags: Dict[str, Any], rc: int + ) -> None: """Receive a CONNACK message from the server.""" client.subscribe( [ @@ -114,11 +117,13 @@ def _cb_on_connect(self, client, userdata, flags, rc): self.broker_active = True - def _cb_on_disconnect(self, client, userdata, rc): + def _cb_on_disconnect(self, client: mqtt, userdata: Any, rc: int) -> None: """Receive notice the MQTT connection has disconnected.""" self.broker_active = False - def _cb_on_message(self, client, userdata, msg): + def _cb_on_message( + self, client: mqtt, userdata: Any, msg: mqtt.MQTTMessage + ) -> None: """Receive a PUBLISH message from the server.""" payload = MQTTPayload(msg.payload) @@ -160,7 +165,9 @@ def current_usage(self, resource: Dict[str, Any]) -> Dict[str, Any]: data = response.json() return data - def register_sensor(self, sensor, resource): + def register_sensor( + self, sensor: GlowConsumptionCurrent, resource: Dict[str, Any] + ) -> None: """Register a live sensor for dispatching MQTT messages.""" self.sensors[resource["classifier"]] = sensor diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index 70bd4a4..3942ae5 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -1,7 +1,7 @@ """Helper classes for Zigbee Smart Energy Profile data.""" import json from enum import Enum -from typing import Any, Dict +from typing import Any, Dict, Optional class Meter: @@ -17,19 +17,19 @@ class SupplyStatus(Enum): ARMED = "01" ON = "02" - current_summation_delivered: int + current_summation_delivered: Optional[int] """Import energy usage""" - current_summation_received: int + current_summation_received: Optional[int] """Export energy usage""" - current_max_demand_delivered: int + current_max_demand_delivered: Optional[int] """Maximum import energy usage rate""" - reading_snapshot_time: str + reading_snapshot_time: Optional[int] """Last time all of the reported attributed were updated""" - supply_status: SupplyStatus + supply_status: Optional[SupplyStatus] """Current state of the meter's supply.""" def __init__(self, payload: Dict[str, Any]): @@ -65,7 +65,7 @@ def __init__(self, payload: Dict[str, Any]): class MeterStatus: """Information about the meter's error conditions.""" - status: str + status: Optional[str] """Meter error conditions""" def __init__(self, payload: Dict[str, Any]): @@ -89,31 +89,31 @@ class MeteringDeviceType(Enum): ELECTRIC = "00" GAS = "80" - unit_of_measure: UnitofMeasure + unit_of_measure: Optional[UnitofMeasure] """Unit for the measured value.""" - multiplier: int + multiplier: Optional[int] """Multiplier value for smart meter readings.""" - divisor: int + divisor: Optional[int] """Divisor value for smart meter readings.""" - summation_formatting: str + summation_formatting: Optional[str] """Bitmap representing decimal places in Summation readings.""" - demand_formatting: str + demand_formatting: Optional[str] """Bitmap representing decimal places in Demand readings.""" - metering_device_type: MeteringDeviceType + metering_device_type: Optional[MeteringDeviceType] """Smart meter device type.""" - siteID: str + siteID: Optional[str] """Electricicity MPAN / Gas MPRN.""" - meter_serial_number: str + meter_serial_number: Optional[str] """Smart meter serial number.""" - alternative_unit_of_measure: UnitofMeasure + alternative_unit_of_measure: Optional[UnitofMeasure] """Alternative unit for the measured value.""" def __init__(self, payload: Dict[str, Any]): @@ -139,16 +139,16 @@ def __init__(self, payload: Dict[str, Any]): class HistoricalConsumption: """Information about the meter's historical readings.""" - instantaneous_demand: int + instantaneous_demand: Optional[int] """Instantaneous import energy usage rate""" - current_day_consumption_delivered: int + current_day_consumption_delivered: Optional[int] """Import energy used in the current day.""" - current_week_consumption_delivered: int + current_week_consumption_delivered: Optional[int] """Import energy used in the current week.""" - current_month_consumption_delivered: int + current_month_consumption_delivered: Optional[int] """Import energy used in the current month.""" def __init__(self, payload: Dict[str, Any]): @@ -181,13 +181,13 @@ def __init__(self, payload: Dict[str, Any]): class AlternativeHistoricalConsumption: """Information about the meter's altenative historical readings.""" - current_day_consumption_delivered: int + current_day_consumption_delivered: Optional[int] """Import energy used in the current day.""" - current_week_consumption_delivered: int + current_week_consumption_delivered: Optional[int] """Import energy used in the current week.""" - current_month_consumption_delivered: int + current_month_consumption_delivered: Optional[int] """Import energy used in the current month.""" def __init__(self, payload: Dict[str, Any]): @@ -232,15 +232,15 @@ def __init__(self, payload: Dict[str, Any]): class MQTTPayload: """Object representing a payload received over MQTT.""" - electricity: Meter + electricity: Optional[Meter] """Data interpreted from an electricity meter.""" - gas: Meter + gas: Optional[Meter] """Data interpreted from a gas meter.""" - def __init__(self, payload: str): + def __init__(self, input: str): """Create internal Meter instances based off the unprocessed payload.""" - payload = json.loads(payload) + payload: Dict[str, Any] = json.loads(input) self.electricity = ( Meter(payload["elecMtr"]) if "03" in payload["elecMtr"]["0702"] else None ) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index b493e63..683487a 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -8,7 +8,7 @@ from .const import DOMAIN from .glow import Glow, InvalidAuth -from .mqttpayload import Meter +from .mqttpayload import Meter, MQTTPayload async def async_setup_entry( @@ -89,7 +89,7 @@ def device_info(self) -> Optional[Dict[str, Any]]: } @property - def state(self) -> Optional[str]: + def state(self) -> Optional[int]: """Return the state of the sensor.""" if self._state: if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": @@ -97,10 +97,9 @@ def state(self) -> Optional[str]: elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": alt = self._state.alternative_historical_consumption return alt.current_day_consumption_delivered - else: - return None + return None - def update_state(self, meter) -> None: + def update_state(self, meter: MQTTPayload) -> None: """Receive an MQTT update from Glow and update the internal state.""" self._state = meter.electricity self.async_write_ha_state() @@ -118,5 +117,5 @@ def unit_of_measurement(self) -> Optional[str]: return POWER_WATT elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": return VOLUME_CUBIC_METERS - else: - return None + + return None diff --git a/setup.cfg b/setup.cfg index bc86215..2adbb5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,6 @@ ignore_errors = True [mypy-voluptuous] ignore_missing_imports = True + +[mypy-paho.*] +ignore_missing_imports = True From 367d610fd63a27475a195d35458ef9d5ec4c8fbb Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sat, 5 Jun 2021 16:04:03 +0100 Subject: [PATCH 15/26] manifest: version 0.1.1 --- custom_components/hildebrandglow/manifest.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/manifest.json b/custom_components/hildebrandglow/manifest.json index 8914feb..b60564d 100644 --- a/custom_components/hildebrandglow/manifest.json +++ b/custom_components/hildebrandglow/manifest.json @@ -8,5 +8,7 @@ "zeroconf": [], "homekit": {}, "dependencies": [], - "codeowners": ["@unlobito"] + "codeowners": ["@unlobito"], + "iot_class": "cloud_polling", + "version": "0.1.1" } From 8ef0a4621e42bf7e7b6f9f3378dd01cb11e24770 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sat, 5 Jun 2021 16:07:28 +0100 Subject: [PATCH 16/26] use MQTT wildcard for topic thanks https://forum.glowmarkt.com/index.php?p=/profile/Dougie ! --- custom_components/hildebrandglow/glow.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index eda0785..f3baa54 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -19,8 +19,7 @@ class Glow: BASE_URL = "https://api.glowmarkt.com/api/v0-1" HILDEBRAND_MQTT_HOST = "glowmqtt.energyhive.com" - HILDEBRAND_MQTT_TOPIC = "SMART/HILD/{hardwareId}" - HILDEBRAND_MQTT_LEGACY = "SMART/DCAD/{hardwareId}" + HILDEBRAND_MQTT_TOPIC = "SMART/+/{hardwareId}" username: str password: str @@ -106,13 +105,7 @@ def _cb_on_connect( ) -> None: """Receive a CONNACK message from the server.""" client.subscribe( - [ - (self.HILDEBRAND_MQTT_TOPIC.format(hardwareId=self.hardwareId), 0), - ( - self.HILDEBRAND_MQTT_LEGACY.format(hardwareId=self.hardwareId), - 0, - ), - ] + [(self.HILDEBRAND_MQTT_TOPIC.format(hardwareId=self.hardwareId), 0)] ) self.broker_active = True From ce7bb34fa0389151d5f81fba58f4ec0e3707336f Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Tue, 15 Jun 2021 18:18:24 +0100 Subject: [PATCH 17/26] chore: device_info() returns DeviceInfo --- custom_components/hildebrandglow/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 683487a..79a9b29 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_POWER, POWER_WATT, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .glow import Glow, InvalidAuth @@ -76,7 +76,7 @@ def icon(self) -> Optional[str]: return None @property - def device_info(self) -> Optional[Dict[str, Any]]: + def device_info(self) -> Optional[DeviceInfo]: """Return information about the sensor data source.""" if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": human_type = "electricity" From 2e7c3bdd644b9dc7260254547e158d72f031e43e Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Tue, 15 Jun 2021 18:18:57 +0100 Subject: [PATCH 18/26] search for the Glow Display deviceTypeId --- custom_components/hildebrandglow/glow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index f3baa54..7ffedd0 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -83,11 +83,18 @@ def retrieve_devices(self) -> List[Dict[str, Any]]: 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" + ZIGBEE_GLOW_DISPLAY_SMETS2 = "b91cf82f-aafe-47f4-930a-b2ed1c7b2691" devices = self.retrieve_devices() cad: Dict[str, Any] = next( - (dev for dev in devices if dev["deviceTypeId"] == ZIGBEE_GLOW_STICK), {} + ( + dev + for dev in devices + if dev["deviceTypeId"] + in [ZIGBEE_GLOW_STICK, ZIGBEE_GLOW_DISPLAY_SMETS2] + ), + {}, ) self.hardwareId = cad["hardwareId"] From 17562bbade55d55a2eb4c91e4463962ff487c3b2 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Wed, 8 Sep 2021 13:45:00 +0100 Subject: [PATCH 19/26] call async_write_ha_state with a job --- custom_components/hildebrandglow/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 79a9b29..2f83785 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -102,7 +102,7 @@ def state(self) -> Optional[int]: def update_state(self, meter: MQTTPayload) -> None: """Receive an MQTT update from Glow and update the internal state.""" self._state = meter.electricity - self.async_write_ha_state() + self.hass.add_job(self.async_write_ha_state) @property def device_class(self) -> str: From d4b9ae35558a8463ec989aa4b8dc998f77312b51 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sat, 11 Sep 2021 20:54:52 +0100 Subject: [PATCH 20/26] emit log messages for known setup failures --- custom_components/hildebrandglow/__init__.py | 12 +++++++++++- custom_components/hildebrandglow/glow.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index 4428dde..a6bcbe4 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -1,5 +1,6 @@ """The Hildebrand Glow integration.""" import asyncio +import logging from typing import Any, Dict import voluptuous as vol @@ -7,7 +8,9 @@ from homeassistant.core import HomeAssistant from .const import APP_ID, DOMAIN -from .glow import Glow, InvalidAuth +from .glow import Glow, InvalidAuth, NoCADAvailable + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -34,6 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: continue except InvalidAuth: + _LOGGER.error("Couldn't login with the provided username/password.") + + return False + + except NoCADAvailable: + _LOGGER.error("Couldn't find any CAD devices (e.g. Glow Stick)") + return False hass.data[DOMAIN][entry.entry_id] = glow diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow.py index 7ffedd0..371f9a4 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow.py @@ -97,9 +97,12 @@ def retrieve_cad_hardwareId(self) -> str: {}, ) - self.hardwareId = cad["hardwareId"] + if "hardwareId" in cad: + self.hardwareId = cad["hardwareId"] - return self.hardwareId + return self.hardwareId + else: + raise NoCADAvailable def connect_mqtt(self) -> None: """Connect the internal MQTT client to the discovered CAD.""" @@ -178,3 +181,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NoCADAvailable(exceptions.HomeAssistantError): + """Error to indicate no CADs were found.""" From ae6ec9a8687b5c1806e2fd86598b4cf9c74eb61f Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Mon, 4 Oct 2021 23:58:20 +0100 Subject: [PATCH 21/26] README: MQTT access --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00fe294..b3a951a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # ha-hildebrandglow HomeAssistant integration for the [Hildebrand Glow](https://www.hildebrand.co.uk/our-products/) smart meter HAN for UK SMETS meters. -Before using this integration, you'll need to have an active Glow account (usable through the Bright app) and API access enabled. If you haven't been given an API Application ID by Hildebrand, you'll need to contact them and request API access be enabled for your account. +You'll need to have an active Glow account (usable through the Bright app), [a Consumer Access Device](https://www.hildebrand.co.uk/our-products/), and MQTT access enabled before using this integration. If you haven't been given MQTT connection details by Hildebrand, you'll need to contact them and request MQTT access be enabled for your account. This integration will currently emit one sensor for the current usage of each detected smart meter. From 6bdf84c8cdb0259908acda0b49955df03446b413 Mon Sep 17 00:00:00 2001 From: Dan Streeter Date: Thu, 14 Oct 2021 13:50:52 +0100 Subject: [PATCH 22/26] =?UTF-8?q?Initial=20WIP=20potential=20fix=20for=20t?= =?UTF-8?q?wos=20complement=20calculation=20of=20consumpt=E2=80=A6=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/hildebrandglow/mqttpayload.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index 3942ae5..406e5dd 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -1,5 +1,6 @@ """Helper classes for Zigbee Smart Energy Profile data.""" import json +import struct from enum import Enum from typing import Any, Dict, Optional @@ -158,7 +159,7 @@ def __init__(self, payload: Dict[str, Any]): ) self.instantaneous_demand = ( - int(historical_consumption["00"], 16) + self._hex_twos_complement_to_decimal(historical_consumption["00"]) if "00" in historical_consumption else None ) @@ -178,6 +179,10 @@ def __init__(self, payload: Dict[str, Any]): else None ) + def _hex_twos_complement_to_decimal(self, hex_value: str) -> int: + """Perform signed 2's complement conversion.""" + return int(struct.unpack(">i", bytes.fromhex(hex_value))[0]) + class AlternativeHistoricalConsumption: """Information about the meter's altenative historical readings.""" From ab265bdb22c73ae7ceab8a550dfc34e38b3bccd7 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Tue, 26 Oct 2021 18:40:25 +0100 Subject: [PATCH 23/26] Check hex length before dec conversion --- custom_components/hildebrandglow/mqttpayload.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/mqttpayload.py index 406e5dd..10f99c3 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/mqttpayload.py @@ -181,7 +181,9 @@ def __init__(self, payload: Dict[str, Any]): def _hex_twos_complement_to_decimal(self, hex_value: str) -> int: """Perform signed 2's complement conversion.""" - return int(struct.unpack(">i", bytes.fromhex(hex_value))[0]) + if len(hex_value) == 8: + return int(struct.unpack(">i", bytes.fromhex(hex_value))[0]) + return int(hex_value, 16) class AlternativeHistoricalConsumption: """Information about the meter's altenative historical readings.""" From e0ad1feb5a04e630f00134fcbbe0bcdd3820e311 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sun, 19 Dec 2021 19:33:36 +0000 Subject: [PATCH 24/26] introduce Glowdata for generic access from sensors heavily inspired by p1monitor and roombapy's design patterns --- .travis.yml | 2 +- custom_components/hildebrandglow/__init__.py | 99 ++++++---- custom_components/hildebrandglow/const.py | 10 +- .../{glow.py => glow/__init__.py} | 27 +-- .../hildebrandglow/glow/glowdata.py | 44 +++++ .../hildebrandglow/{ => glow}/mqttpayload.py | 8 +- custom_components/hildebrandglow/sensor.py | 184 ++++++++---------- requirements-dev.txt | 2 + setup.cfg | 8 +- 9 files changed, 226 insertions(+), 158 deletions(-) rename custom_components/hildebrandglow/{glow.py => glow/__init__.py} (90%) create mode 100644 custom_components/hildebrandglow/glow/glowdata.py rename custom_components/hildebrandglow/{ => glow}/mqttpayload.py (98%) diff --git a/.travis.yml b/.travis.yml index 95dd0d5..ab4dd09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.8" + - "3.9" install: # Attempt to work around Home Assistant not being declared through PEP 561 # Details: https://github.com/home-assistant/core/pull/28866#pullrequestreview-319309922 diff --git a/custom_components/hildebrandglow/__init__.py b/custom_components/hildebrandglow/__init__.py index a6bcbe4..47965c2 100644 --- a/custom_components/hildebrandglow/__init__.py +++ b/custom_components/hildebrandglow/__init__.py @@ -1,20 +1,24 @@ """The Hildebrand Glow integration.""" import asyncio -import logging from typing import Any, Dict +import async_timeout import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + InvalidStateError, +) -from .const import APP_ID, DOMAIN -from .glow import Glow, InvalidAuth, NoCADAvailable - -_LOGGER = logging.getLogger(__name__) +from .const import APP_ID, DOMAIN, GLOW_SESSION, LOGGER +from .glow import CannotConnect, Glow, InvalidAuth, NoCADAvailable CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) -PLATFORMS = ["sensor"] +PLATFORMS = (SENSOR_DOMAIN,) async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool: @@ -26,47 +30,74 @@ 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(APP_ID, entry.data["username"], entry.data["password"]) + 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) + if not await async_connect_or_timeout(hass, glow): + return False + except CannotConnect as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {GLOW_SESSION: glow} + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + if not entry.update_listeners: + entry.add_update_listener(async_update_options) + + return True - while not glow.broker_active: - continue - except InvalidAuth: - _LOGGER.error("Couldn't login with the provided username/password.") +async def async_connect_or_timeout(hass: HomeAssistant, glow: Glow) -> bool: + """Connect from Glow.""" + try: + async with async_timeout.timeout(10): + LOGGER.debug("Initialize connection from Glow") + + 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) - return False + while not glow.broker_active: + await asyncio.sleep(1) + except InvalidAuth as err: + LOGGER.error("Couldn't login with the provided username/password") + raise ConfigEntryAuthFailed from err - except NoCADAvailable: - _LOGGER.error("Couldn't find any CAD devices (e.g. Glow Stick)") + except NoCADAvailable as err: + LOGGER.error("Couldn't find any CAD devices (e.g. Glow Stick)") + raise InvalidStateError from err - return False + except asyncio.TimeoutError as err: + await async_disconnect_or_timeout(hass, glow) + LOGGER.debug("Timeout expired: %s", err) + raise CannotConnect from err - hass.data[DOMAIN][entry.entry_id] = glow + return True - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) +async def async_disconnect_or_timeout(hass: HomeAssistant, glow: Glow) -> bool: + """Disconnect from Glow.""" + LOGGER.debug("Disconnect from Glow") + async with async_timeout.timeout(3): + await hass.async_add_executor_job(glow.disconnect) return True +async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, component) - for component in PLATFORMS - ] - ) - ) + """Unload Hildebrand Glow config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - + coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator[GLOW_SESSION].disconnect() return unload_ok diff --git a/custom_components/hildebrandglow/const.py b/custom_components/hildebrandglow/const.py index 1d2b3e3..060afd3 100644 --- a/custom_components/hildebrandglow/const.py +++ b/custom_components/hildebrandglow/const.py @@ -1,4 +1,10 @@ """Constants for the Hildebrand Glow integration.""" +import logging +from typing import Final -DOMAIN = "hildebrandglow" -APP_ID = "b0f1b774-a586-4f72-9edd-27ead8aa7a8d" +DOMAIN: Final = "hildebrandglow" +APP_ID: Final = "b0f1b774-a586-4f72-9edd-27ead8aa7a8d" + +LOGGER = logging.getLogger(__package__) + +GLOW_SESSION: Final = "glow_session" diff --git a/custom_components/hildebrandglow/glow.py b/custom_components/hildebrandglow/glow/__init__.py similarity index 90% rename from custom_components/hildebrandglow/glow.py rename to custom_components/hildebrandglow/glow/__init__.py index 371f9a4..554bae7 100644 --- a/custom_components/hildebrandglow/glow.py +++ b/custom_components/hildebrandglow/glow/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations from pprint import pprint -from typing import TYPE_CHECKING, Any, Dict, List +from typing import Any, Callable, Dict, List import paho.mqtt.client as mqtt import requests @@ -10,8 +10,7 @@ from .mqttpayload import MQTTPayload -if TYPE_CHECKING: - from .sensor import GlowConsumptionCurrent +from .glowdata import SmartMeter # isort:skip class Glow: @@ -29,7 +28,9 @@ class Glow: hardwareId: str broker: mqtt.Client - sensors: Dict[str, GlowConsumptionCurrent] = {} + data: SmartMeter = SmartMeter(None, None, None) + + callbacks: List[Callable] = [] def __init__(self, app_id: str, username: str, password: str): """Create an authenticated Glow object.""" @@ -110,6 +111,10 @@ def connect_mqtt(self) -> None: self.broker.loop_start() + async def disconnect(self) -> None: + """Disconnect the internal MQTT client.""" + return self.broker.loop_stop() + def _cb_on_connect( self, client: mqtt, userdata: Any, flags: Dict[str, Any], rc: int ) -> None: @@ -129,12 +134,10 @@ def _cb_on_message( ) -> None: """Receive a PUBLISH message from the server.""" payload = MQTTPayload(msg.payload) + self.data = SmartMeter.from_mqtt_payload(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) + for callback in self.callbacks: + callback(payload) def retrieve_resources(self) -> List[Dict[str, Any]]: """Retrieve the resources known to Glowmarkt for the authenticated user.""" @@ -168,11 +171,9 @@ def current_usage(self, resource: Dict[str, Any]) -> Dict[str, Any]: data = response.json() return data - def register_sensor( - self, sensor: GlowConsumptionCurrent, resource: Dict[str, Any] - ) -> None: + def register_on_message_callback(self, callback: Callable) -> None: """Register a live sensor for dispatching MQTT messages.""" - self.sensors[resource["classifier"]] = sensor + self.callbacks.append(callback) class CannotConnect(exceptions.HomeAssistantError): diff --git a/custom_components/hildebrandglow/glow/glowdata.py b/custom_components/hildebrandglow/glow/glowdata.py new file mode 100644 index 0000000..2953394 --- /dev/null +++ b/custom_components/hildebrandglow/glow/glowdata.py @@ -0,0 +1,44 @@ +"""Data classes for interpreted Glow structures.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from . import MQTTPayload + + +@dataclass +class SmartMeter: + """Data object for platform agnostic smart metering information.""" + + gas_consumption: float | None + + power_consumption: int | None + energy_consumption: float | None + + @staticmethod + def from_mqtt_payload(data: MQTTPayload) -> SmartMeter: + """Populate SmartMeter object from an MQTTPayload object.""" + meter = SmartMeter( + gas_consumption=None, + power_consumption=None, + energy_consumption=None, + ) + + if data.gas: + ahc = data.gas.alternative_historical_consumption + meter.gas_consumption = ahc.current_day_consumption_delivered + + if data.electricity: + meter.power_consumption = ( + data.electricity.historical_consumption.instantaneous_demand + ) + + if data.electricity.reading_information_set.current_summation_delivered: + meter.energy_consumption = ( + data.electricity.reading_information_set.current_summation_delivered + * data.electricity.formatting.multiplier + / data.electricity.formatting.divisor + ) + + return meter diff --git a/custom_components/hildebrandglow/mqttpayload.py b/custom_components/hildebrandglow/glow/mqttpayload.py similarity index 98% rename from custom_components/hildebrandglow/mqttpayload.py rename to custom_components/hildebrandglow/glow/mqttpayload.py index 10f99c3..de7b18f 100644 --- a/custom_components/hildebrandglow/mqttpayload.py +++ b/custom_components/hildebrandglow/glow/mqttpayload.py @@ -93,10 +93,10 @@ class MeteringDeviceType(Enum): unit_of_measure: Optional[UnitofMeasure] """Unit for the measured value.""" - multiplier: Optional[int] + multiplier: int """Multiplier value for smart meter readings.""" - divisor: Optional[int] + divisor: int """Divisor value for smart meter readings.""" summation_formatting: Optional[str] @@ -122,8 +122,8 @@ def __init__(self, payload: Dict[str, Any]): formatting = payload["0702"]["03"] if "03" in payload["0702"] else {} self.unit_of_measure = self.UnitofMeasure(formatting.get("00", "00")) - self.multiplier = int(formatting["01"], 16) if "01" in formatting else None - self.divisor = int(formatting["02"], 16) if "02" in formatting else None + self.multiplier = int(formatting["01"], 16) if "01" in formatting else 1 + self.divisor = int(formatting["02"], 16) if "02" in formatting else 1 self.summation_formatting = formatting.get("03") self.demand_formatting = formatting.get("04") self.metering_device_type = ( diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index 2f83785..d5735f0 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -1,121 +1,109 @@ """Platform for sensor integration.""" -from typing import Any, Callable, Dict, Optional - +from typing import Any, Callable + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_CLASS_POWER, POWER_WATT, VOLUME_CUBIC_METERS +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + VOLUME_CUBIC_METERS, +) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, Entity - -from .const import DOMAIN -from .glow import Glow, InvalidAuth -from .mqttpayload import Meter, MQTTPayload +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN, GLOW_SESSION +from .glow import Glow + +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="gas_consumption", + name="Gas Consumption", + entity_registry_enabled_default=False, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power_consumption", + name="Power Consumption", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="energy_consumption", + name="Energy Consumption", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), +) async def async_setup_entry( hass: HomeAssistant, config: ConfigEntry, async_add_entities: Callable -) -> bool: +) -> None: """Set up the sensor platform.""" - new_entities = [] - - for entry in hass.data[DOMAIN]: - glow = hass.data[DOMAIN][entry] - - resources: dict = {} - - try: - resources = await hass.async_add_executor_job(glow.retrieve_resources) - except InvalidAuth: - return False - - for resource in resources: - if resource["classifier"] in GlowConsumptionCurrent.knownClassifiers: - sensor = GlowConsumptionCurrent(glow, resource) - glow.register_sensor(sensor, resource) - new_entities.append(sensor) + async_add_entities( + GlowSensorEntity( + glow=hass.data[DOMAIN][config.entry_id][GLOW_SESSION], + description=description, + ) + for description in SENSORS + ) - async_add_entities(new_entities) - return True - - -class GlowConsumptionCurrent(Entity): +class GlowSensorEntity(SensorEntity): """Sensor object for the Glowmarkt resource's current consumption.""" - hass: HomeAssistant - - knownClassifiers = ["gas.consumption", "electricity.consumption"] - - _state: Optional[Meter] - available = True should_poll = False - def __init__(self, glow: Glow, resource: Dict[str, Any]): + glow: Glow + + def __init__( + self, + *, + glow: Glow, + description: SensorEntityDescription, + ) -> None: """Initialize the sensor.""" - self._state = None + super().__init__() self.glow = glow - self.resource = resource - - @property - def unique_id(self) -> str: - """Return a unique identifier string for the sensor.""" - return self.resource["resourceId"] - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return self.resource["label"] - @property - def icon(self) -> Optional[str]: - """Icon to use in the frontend, if any.""" - if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": - return "mdi:flash" - elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": - return "mdi:fire" - else: - return None - - @property - def device_info(self) -> Optional[DeviceInfo]: - """Return information about the sensor data source.""" - if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": - human_type = "electricity" - elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": - human_type = "gas" + self.entity_id = f"{SENSOR_DOMAIN}.glow{glow.hardwareId}_{description.key}" + self.entity_description = description + self._attr_unique_id = f"glow{glow.hardwareId}_{description.key}" - return { - "identifiers": {(DOMAIN, self.resource["resourceId"])}, - "name": f"Smart Meter, {human_type}", - } + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, f"glow{glow.hardwareId}")}, + manufacturer="Hildebrand", + name="Smart Meter", + ) - @property - def state(self) -> Optional[int]: - """Return the state of the sensor.""" - if self._state: - if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": - return self._state.historical_consumption.instantaneous_demand - elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": - alt = self._state.alternative_historical_consumption - return alt.current_day_consumption_delivered - return None + async def async_added_to_hass(self) -> None: + """Register callback function.""" + self.glow.register_on_message_callback(self.on_message) - def update_state(self, meter: MQTTPayload) -> None: - """Receive an MQTT update from Glow and update the internal state.""" - self._state = meter.electricity + def on_message(self, message: Any) -> None: + """Receive callback for incoming MQTT payloads.""" self.hass.add_job(self.async_write_ha_state) @property - def device_class(self) -> str: - """Return the device class (always DEVICE_CLASS_POWER).""" - return DEVICE_CLASS_POWER - - @property - def unit_of_measurement(self) -> Optional[str]: - """Return the unit of measurement.""" - if self._state is not None: - if self.resource["dataSourceResourceTypeInfo"]["type"] == "ELEC": - return POWER_WATT - elif self.resource["dataSourceResourceTypeInfo"]["type"] == "GAS": - return VOLUME_CUBIC_METERS - - return None + def native_value(self) -> StateType: + """Return the state of the sensor.""" + value = getattr(self.glow.data, self.entity_description.key) + if isinstance(value, str): + return value.lower() + return value diff --git a/requirements-dev.txt b/requirements-dev.txt index f2e682a..2f25f02 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,6 @@ black==21.4b0 flake8==3.9.1 isort==5.8.0 mypy==0.812 +pycodestyle==2.7.0 +pyflakes==2.3.1 paho-mqtt==1.5.1 diff --git a/setup.cfg b/setup.cfg index 2adbb5a..cbbf7a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,9 @@ [flake8] -extend-ignore = E203, E231, max-line-length = 88 +extend-ignore = E203 [isort] -combine_as_imports = True -force_grid_wrap = 0 -include_trailing_comma = True -line_length = 88 -multi_line_output = 3 +profile = black known_third_party = homeassistant,voluptuous [mypy] From 1ca3602bc8fa065bb2beb27df41389f936f1f7e0 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Tue, 21 Dec 2021 19:14:11 +0000 Subject: [PATCH 25/26] Determine sensor availability at runtime attempts to fix #46 --- custom_components/hildebrandglow/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/custom_components/hildebrandglow/sensor.py b/custom_components/hildebrandglow/sensor.py index d5735f0..ffbd7cc 100644 --- a/custom_components/hildebrandglow/sensor.py +++ b/custom_components/hildebrandglow/sensor.py @@ -29,7 +29,6 @@ SensorEntityDescription( key="gas_consumption", name="Gas Consumption", - entity_registry_enabled_default=False, native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, @@ -100,6 +99,11 @@ def on_message(self, message: Any) -> None: """Receive callback for incoming MQTT payloads.""" self.hass.add_job(self.async_write_ha_state) + @property + def available(self) -> bool: + """Return the sensor's availability.""" + return getattr(self.glow.data, self.entity_description.key) is not None + @property def native_value(self) -> StateType: """Return the state of the sensor.""" From 845bc48ca4fb3348370c6f85a6f39393f09ba176 Mon Sep 17 00:00:00 2001 From: Harley Watson Date: Sat, 30 Apr 2022 17:45:09 +0100 Subject: [PATCH 26/26] README.md: archived --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index b3a951a..d11ef1f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ +# Archived +This repository was archived on 30 April 2022 (see https://github.com/unlobito/ha-hildebrandglow/discussions/55 for details). + +[HandyHat/ha-hildebrandglow-dcc](https://github.com/HandyHat/ha-hildebrandglow-dcc) is maintained as a fork for DCC users. + # ha-hildebrandglow + HomeAssistant integration for the [Hildebrand Glow](https://www.hildebrand.co.uk/our-products/) smart meter HAN for UK SMETS meters. You'll need to have an active Glow account (usable through the Bright app), [a Consumer Access Device](https://www.hildebrand.co.uk/our-products/), and MQTT access enabled before using this integration. If you haven't been given MQTT connection details by Hildebrand, you'll need to contact them and request MQTT access be enabled for your account.