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

Commit

Permalink
introduce Glowdata for generic access from sensors
Browse files Browse the repository at this point in the history
heavily inspired by p1monitor and roombapy's design patterns
  • Loading branch information
unlobito committed Dec 19, 2021
1 parent ab265bd commit e0ad1fe
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 158 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
99 changes: 65 additions & 34 deletions custom_components/hildebrandglow/__init__.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
10 changes: 8 additions & 2 deletions custom_components/hildebrandglow/const.py
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
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
from homeassistant import exceptions

from .mqttpayload import MQTTPayload

if TYPE_CHECKING:
from .sensor import GlowConsumptionCurrent
from .glowdata import SmartMeter # isort:skip


class Glow:
Expand All @@ -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."""
Expand Down Expand Up @@ -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:
Expand All @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down
44 changes: 44 additions & 0 deletions custom_components/hildebrandglow/glow/glowdata.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 = (
Expand Down
Loading

0 comments on commit e0ad1fe

Please sign in to comment.