Tweak geniushub and bump client to v0.6.26 (#26640)

* use state attribute rather than type

* HA style tweaks

* small tweak

* bump client

* add more device_state_attributes

* bump client

* small tweak

* bump client for concurrent IO

* force snake_case, and refactor (consolidate) Devices/Zones

* force snake_case, and refactor (consolidate) Devices/Zones 2

* force snake_case, and refactor (consolidate) Devices/Zones 3

* refactor last_comms / wakeup_interval check

* movement sensor is dynamic, and tweaking

* tweak

* bump client to v0.6.20

* dummy

* dummy 2

* bump client to handle another edge case

* use entity_id fro zones

* small tweak

* bump client to 0.6.22

* add recursive snake_case converter

* fix regression

* fix regression 2

* fix regression 3

* remove Awaitables

* don't dynamically create function every scan_interval

* log kast_comms as localtime, delint dt_util

* add sensors fro v1 API

* tweak entity_id

* bump client

* bump client to v0.6.24

* bump client to v0.6.25

* explicit device attrs, dt as UTC

* add unique_id, remove entity_id

* Bump client to 0.6.26 - add Hub UID

* remove convert_dict()

* add mac_address (uid) for v1 API

* tweak var names

* add UID.upper() to avoid unwanted unique_id changes

* Update homeassistant/components/geniushub/__init__.py

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Update homeassistant/components/geniushub/__init__.py

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* remove underscores

* refactor for broker

* ready now

* validate UID (MAC address)

* move uid to broker

* use existing constant

* pass client to broker
This commit is contained in:
David Bonnes 2019-10-02 17:27:13 +01:00 committed by GitHub
parent c7da781efc
commit c78b3a4439
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 283 additions and 255 deletions

View file

@ -1,14 +1,22 @@
"""Support for a Genius Hub system.""" """Support for a Genius Hub system."""
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Awaitable from typing import Any, Dict, Optional
import aiohttp import aiohttp
import voluptuous as vol import voluptuous as vol
from geniushubclient import GeniusHub from geniushubclient import GeniusHub
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_HOST,
CONF_MAC,
CONF_PASSWORD,
CONF_TOKEN,
CONF_USERNAME,
TEMP_CELSIUS,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -19,39 +27,66 @@ from homeassistant.helpers.dispatcher import (
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
ATTR_DURATION = "duration"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "geniushub" DOMAIN = "geniushub"
# temperature is repeated here, as it gives access to high-precision temps
GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"]
GH_DEVICE_ATTRS = {
"luminance": "luminance",
"measuredTemperature": "measured_temperature",
"occupancyTrigger": "occupancy_trigger",
"setback": "setback",
"setTemperature": "set_temperature",
"wakeupInterval": "wakeup_interval",
}
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$"
_V3_API_SCHEMA = vol.Schema(
V1_API_SCHEMA = vol.Schema(
{
vol.Required(CONF_TOKEN): cv.string,
vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP),
}
)
V3_API_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP),
} }
) )
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Any(_V3_API_SCHEMA, _V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA
) )
async def async_setup(hass, hass_config): async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Create a Genius Hub system.""" """Create a Genius Hub system."""
kwargs = dict(hass_config[DOMAIN]) hass.data[DOMAIN] = {}
kwargs = dict(config[DOMAIN])
if CONF_HOST in kwargs: if CONF_HOST in kwargs:
args = (kwargs.pop(CONF_HOST),) args = (kwargs.pop(CONF_HOST),)
else: else:
args = (kwargs.pop(CONF_TOKEN),) args = (kwargs.pop(CONF_TOKEN),)
hub_uid = kwargs.pop(CONF_MAC, None)
hass.data[DOMAIN] = {} client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass))
broker = GeniusBroker(hass, args, kwargs)
broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid)
try: try:
await broker.client.update() await client.update()
except aiohttp.ClientResponseError as err: except aiohttp.ClientResponseError as err:
_LOGGER.error("Setup failed, check your configuration, %s", err) _LOGGER.error("Setup failed, check your configuration, %s", err)
return False return False
@ -59,16 +94,8 @@ async def async_setup(hass, hass_config):
async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL)
for platform in ["climate", "water_heater"]: for platform in ["climate", "water_heater", "sensor", "binary_sensor"]:
hass.async_create_task( hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))
async_load_platform(hass, platform, DOMAIN, {}, hass_config)
)
if broker.client.api_version == 3: # pylint: disable=no-member
for platform in ["sensor", "binary_sensor"]:
hass.async_create_task(
async_load_platform(hass, platform, DOMAIN, {}, hass_config)
)
return True return True
@ -76,25 +103,30 @@ async def async_setup(hass, hass_config):
class GeniusBroker: class GeniusBroker:
"""Container for geniushub client and data.""" """Container for geniushub client and data."""
def __init__(self, hass, args, kwargs): def __init__(self, hass, client, hub_uid) -> None:
"""Initialize the geniushub client.""" """Initialize the geniushub client."""
self.hass = hass self.hass = hass
self.client = hass.data[DOMAIN]["client"] = GeniusHub( self.client = client
*args, **kwargs, session=async_get_clientsession(hass) self._hub_uid = hub_uid
)
async def async_update(self, now, **kwargs): @property
def hub_uid(self) -> int:
"""Return the Hub UID (MAC address)."""
# pylint: disable=no-member
return self._hub_uid if self._hub_uid is not None else self.client.uid
async def async_update(self, now, **kwargs) -> None:
"""Update the geniushub client's data.""" """Update the geniushub client's data."""
try: try:
await self.client.update() await self.client.update()
except aiohttp.ClientResponseError as err: except aiohttp.ClientResponseError as err:
_LOGGER.warning("Update failed, %s", err) _LOGGER.warning("Update failed, message is: %s", err)
return return
self.make_debug_log_entries() self.make_debug_log_entries()
async_dispatcher_send(self.hass, DOMAIN) async_dispatcher_send(self.hass, DOMAIN)
def make_debug_log_entries(self): def make_debug_log_entries(self) -> None:
"""Make any useful debug log entries.""" """Make any useful debug log entries."""
# pylint: disable=protected-access # pylint: disable=protected-access
_LOGGER.debug( _LOGGER.debug(
@ -105,13 +137,13 @@ class GeniusBroker:
class GeniusEntity(Entity): class GeniusEntity(Entity):
"""Base for all Genius Hub endtities.""" """Base for all Genius Hub entities."""
def __init__(self): def __init__(self) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._name = None self._unique_id = self._name = None
async def async_added_to_hass(self) -> Awaitable[None]: async def async_added_to_hass(self) -> None:
"""Set up a listener when this entity is added to HA.""" """Set up a listener when this entity is added to HA."""
async_dispatcher_connect(self.hass, DOMAIN, self._refresh) async_dispatcher_connect(self.hass, DOMAIN, self._refresh)
@ -119,6 +151,11 @@ class GeniusEntity(Entity):
def _refresh(self) -> None: def _refresh(self) -> None:
self.async_schedule_update_ha_state(force_refresh=True) self.async_schedule_update_ha_state(force_refresh=True)
@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self._unique_id
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the geniushub entity.""" """Return the name of the geniushub entity."""
@ -128,3 +165,102 @@ class GeniusEntity(Entity):
def should_poll(self) -> bool: def should_poll(self) -> bool:
"""Return False as geniushub entities should not be polled.""" """Return False as geniushub entities should not be polled."""
return False return False
class GeniusDevice(GeniusEntity):
"""Base for all Genius Hub devices."""
def __init__(self, broker, device) -> None:
"""Initialize the Device."""
super().__init__()
self._device = device
self._unique_id = f"{broker.hub_uid}_device_{device.id}"
self._last_comms = self._state_attr = None
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
attrs = {}
attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"]
if self._last_comms:
attrs["last_comms"] = self._last_comms.isoformat()
state = dict(self._device.data["state"])
if "_state" in self._device.data: # only for v3 API
state.update(self._device.data["_state"])
attrs["state"] = {
GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS
}
return attrs
async def async_update(self) -> None:
"""Update an entity's state data."""
if "_state" in self._device.data: # only for v3 API
self._last_comms = dt_util.utc_from_timestamp(
self._device.data["_state"]["lastComms"]
)
class GeniusZone(GeniusEntity):
"""Base for all Genius Hub zones."""
def __init__(self, broker, zone) -> None:
"""Initialize the Zone."""
super().__init__()
self._zone = zone
self._unique_id = f"{broker.hub_uid}_device_{zone.id}"
self._max_temp = self._min_temp = self._supported_features = None
@property
def name(self) -> str:
"""Return the name of the climate device."""
return self._zone.name
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS}
return {"status": status}
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self._zone.data.get("temperature")
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
return self._zone.data["setpoint"]
@property
def min_temp(self) -> float:
"""Return max valid temperature that can be set."""
return self._min_temp
@property
def max_temp(self) -> float:
"""Return max valid temperature that can be set."""
return self._max_temp
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def supported_features(self) -> int:
"""Return the bitmask of supported features."""
return self._supported_features
async def async_set_temperature(self, **kwargs) -> None:
"""Set a new target temperature for this zone."""
await self._zone.set_override(
kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600)
)

View file

@ -1,52 +1,45 @@
"""Support for Genius Hub binary_sensor devices.""" """Support for Genius Hub binary_sensor devices."""
from typing import Any, Dict
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.util.dt import utc_from_timestamp from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from . import DOMAIN, GeniusEntity from . import DOMAIN, GeniusDevice
GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] GH_STATE_ATTR = "outputOnOff"
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
"""Set up the Genius Hub sensor entities.""" """Set up the Genius Hub sensor entities."""
client = hass.data[DOMAIN]["client"] if discovery_info is None:
return
broker = hass.data[DOMAIN]["broker"]
switches = [ switches = [
GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH GeniusBinarySensor(broker, d, GH_STATE_ATTR)
for d in broker.client.device_objs
if GH_STATE_ATTR in d.data["state"]
] ]
async_add_entities(switches) async_add_entities(switches, update_before_add=True)
class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): class GeniusBinarySensor(GeniusDevice, BinarySensorDevice):
"""Representation of a Genius Hub binary_sensor.""" """Representation of a Genius Hub binary_sensor."""
def __init__(self, device) -> None: def __init__(self, broker, device, state_attr) -> None:
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
super().__init__() super().__init__(broker, device)
self._state_attr = state_attr
self._device = device
if device.type[:21] == "Dual Channel Receiver": if device.type[:21] == "Dual Channel Receiver":
self._name = f"Dual Channel Receiver {device.id}" self._name = f"{device.type[:21]} {device.id}"
else: else:
self._name = f"{device.type} {device.id}" self._name = f"{device.type} {device.id}"
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the status of the sensor.""" """Return the status of the sensor."""
return self._device.data["state"]["outputOnOff"] return self._device.data["state"][self._state_attr]
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
attrs = {}
attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"]
# pylint: disable=protected-access
last_comms = self._device._raw["childValues"]["lastComms"]["val"]
if last_comms != 0:
attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat()
return {**attrs}

View file

@ -1,5 +1,5 @@
"""Support for Genius Hub climate devices.""" """Support for Genius Hub climate devices."""
from typing import Any, Awaitable, Dict, Optional, List from typing import Optional, List
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -10,16 +10,9 @@ from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
) )
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from . import DOMAIN, GeniusEntity from . import DOMAIN, GeniusZone
ATTR_DURATION = "duration"
GH_ZONES = ["radiator"]
# temperature is repeated here, as it gives access to high-precision temps
GH_STATE_ATTRS = ["mode", "temperature", "type", "occupied", "override"]
# GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes
HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"}
@ -28,78 +21,43 @@ GH_HVAC_TO_HA = {v: k for k, v in HA_HVAC_TO_GH.items()}
HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"} HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"}
GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()}
GH_ZONES = ["radiator", "wet underfloor"]
async def async_setup_platform( async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
): ) -> None:
"""Set up the Genius Hub climate entities.""" """Set up the Genius Hub climate entities."""
client = hass.data[DOMAIN]["client"] if discovery_info is None:
return
entities = [ broker = hass.data[DOMAIN]["broker"]
GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES
] async_add_entities(
async_add_entities(entities) [
GeniusClimateZone(broker, z)
for z in broker.client.zone_objs
if z.data["type"] in GH_ZONES
]
)
class GeniusClimateZone(GeniusEntity, ClimateDevice): class GeniusClimateZone(GeniusZone, ClimateDevice):
"""Representation of a Genius Hub climate device.""" """Representation of a Genius Hub climate device."""
def __init__(self, zone) -> None: def __init__(self, broker, zone) -> None:
"""Initialize the climate device.""" """Initialize the climate device."""
super().__init__() super().__init__(broker, zone)
self._zone = zone self._max_temp = 28.0
if hasattr(self._zone, "occupied"): # has a movement sensor self._min_temp = 4.0
self._preset_modes = list(HA_PRESET_TO_GH) self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
else:
self._preset_modes = [PRESET_BOOST]
@property
def name(self) -> str:
"""Return the name of the climate device."""
return self._zone.name
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
tmp = self._zone.data.items()
return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}}
@property @property
def icon(self) -> str: def icon(self) -> str:
"""Return the icon to use in the frontend UI.""" """Return the icon to use in the frontend UI."""
return "mdi:radiator" return "mdi:radiator"
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self._zone.data["temperature"]
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
return self._zone.data["setpoint"]
@property
def min_temp(self) -> float:
"""Return max valid temperature that can be set."""
return 4.0
@property
def max_temp(self) -> float:
"""Return max valid temperature that can be set."""
return 28.0
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
@property @property
def hvac_mode(self) -> str: def hvac_mode(self) -> str:
"""Return hvac operation ie. heat, cool mode.""" """Return hvac operation ie. heat, cool mode."""
@ -118,18 +76,14 @@ class GeniusClimateZone(GeniusEntity, ClimateDevice):
@property @property
def preset_modes(self) -> Optional[List[str]]: def preset_modes(self) -> Optional[List[str]]:
"""Return a list of available preset modes.""" """Return a list of available preset modes."""
return self._preset_modes if "occupied" in self._zone.data: # if has a movement sensor
return [PRESET_ACTIVITY, PRESET_BOOST]
return [PRESET_BOOST]
async def async_set_temperature(self, **kwargs) -> Awaitable[None]: async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Set a new target temperature for this zone."""
await self._zone.set_override(
kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600)
)
async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]:
"""Set a new hvac mode.""" """Set a new hvac mode."""
await self._zone.set_mode(HA_HVAC_TO_GH.get(hvac_mode)) await self._zone.set_mode(HA_HVAC_TO_GH.get(hvac_mode))
async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set a new preset mode.""" """Set a new preset mode."""
await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer")) await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer"))

View file

@ -3,7 +3,7 @@
"name": "Genius Hub", "name": "Genius Hub",
"documentation": "https://www.home-assistant.io/integrations/geniushub", "documentation": "https://www.home-assistant.io/integrations/geniushub",
"requirements": [ "requirements": [
"geniushub-client==0.6.13" "geniushub-client==0.6.26"
], ],
"dependencies": [], "dependencies": [],
"codeowners": ["@zxdavb"] "codeowners": ["@zxdavb"]

View file

@ -1,13 +1,14 @@
"""Support for Genius Hub sensor devices.""" """Support for Genius Hub sensor devices."""
from datetime import timedelta from datetime import timedelta
from typing import Any, Awaitable, Dict from typing import Any, Dict
from homeassistant.const import DEVICE_CLASS_BATTERY from homeassistant.const import DEVICE_CLASS_BATTERY
from homeassistant.util.dt import utc_from_timestamp, utcnow from homeassistant.helpers.typing import ConfigType, HomeAssistantType
import homeassistant.util.dt as dt_util
from . import DOMAIN, GeniusEntity from . import DOMAIN, GeniusDevice, GeniusEntity
GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] GH_STATE_ATTR = "batteryLevel"
GH_LEVEL_MAPPING = { GH_LEVEL_MAPPING = {
"error": "Errors", "error": "Errors",
@ -16,42 +17,47 @@ GH_LEVEL_MAPPING = {
} }
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): async def async_setup_platform(
hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
) -> None:
"""Set up the Genius Hub sensor entities.""" """Set up the Genius Hub sensor entities."""
client = hass.data[DOMAIN]["client"] if discovery_info is None:
return
sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] broker = hass.data[DOMAIN]["broker"]
issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)]
sensors = [
GeniusBattery(broker, d, GH_STATE_ATTR)
for d in broker.client.device_objs
if GH_STATE_ATTR in d.data["state"]
]
issues = [GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)]
async_add_entities(sensors + issues, update_before_add=True) async_add_entities(sensors + issues, update_before_add=True)
class GeniusBattery(GeniusEntity): class GeniusBattery(GeniusDevice):
"""Representation of a Genius Hub sensor.""" """Representation of a Genius Hub sensor."""
def __init__(self, device) -> None: def __init__(self, broker, device, state_attr) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__() super().__init__(broker, device)
self._state_attr = state_attr
self._device = device
self._name = f"{device.type} {device.id}" self._name = f"{device.type} {device.id}"
@property @property
def icon(self) -> str: def icon(self) -> str:
"""Return the icon of the sensor.""" """Return the icon of the sensor."""
if "_state" in self._device.data: # only for v3 API
interval = timedelta(
seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60)
)
if self._last_comms < dt_util.utcnow() - interval * 3:
return "mdi:battery-unknown"
values = self._device._raw["childValues"] # pylint: disable=protected-access battery_level = self._device.data["state"][self._state_attr]
last_comms = utc_from_timestamp(values["lastComms"]["val"])
if "WakeUp_Interval" in values:
interval = timedelta(seconds=values["WakeUp_Interval"]["val"])
else:
interval = timedelta(minutes=20)
if last_comms < utcnow() - interval * 3:
return "mdi:battery-unknown"
battery_level = self._device.data["state"]["batteryLevel"]
if battery_level == 255: if battery_level == 255:
return "mdi:battery-unknown" return "mdi:battery-unknown"
if battery_level < 40: if battery_level < 40:
@ -76,31 +82,19 @@ class GeniusBattery(GeniusEntity):
@property @property
def state(self) -> str: def state(self) -> str:
"""Return the state of the sensor.""" """Return the state of the sensor."""
level = self._device.data["state"].get("batteryLevel", 255) level = self._device.data["state"][self._state_attr]
return level if level != 255 else 0 return level if level != 255 else 0
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
attrs = {}
attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"]
# pylint: disable=protected-access
last_comms = self._device._raw["childValues"]["lastComms"]["val"]
attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat()
return {**attrs}
class GeniusIssue(GeniusEntity): class GeniusIssue(GeniusEntity):
"""Representation of a Genius Hub sensor.""" """Representation of a Genius Hub sensor."""
def __init__(self, hub, level) -> None: def __init__(self, broker, level) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__() super().__init__()
self._hub = hub self._hub = broker.client
self._name = GH_LEVEL_MAPPING[level] self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}"
self._level = level self._level = level
self._issues = [] self._issues = []
@ -114,7 +108,7 @@ class GeniusIssue(GeniusEntity):
"""Return the device state attributes.""" """Return the device state attributes."""
return {f"{self._level}_list": self._issues} return {f"{self._level}_list": self._issues}
async def async_update(self) -> Awaitable[None]: async def async_update(self) -> None:
"""Process the sensor's state data.""" """Process the sensor's state data."""
self._issues = [ self._issues = [
i["description"] for i in self._hub.issues if i["level"] == self._level i["description"] for i in self._hub.issues if i["level"] == self._level

View file

@ -1,27 +1,20 @@
"""Support for Genius Hub water_heater devices.""" """Support for Genius Hub water_heater devices."""
from typing import Any, Awaitable, Dict, Optional, List from typing import List
from homeassistant.components.water_heater import ( from homeassistant.components.water_heater import (
WaterHeaterDevice, WaterHeaterDevice,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_OPERATION_MODE, SUPPORT_OPERATION_MODE,
) )
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS from homeassistant.const import STATE_OFF
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from . import DOMAIN, GeniusEntity from . import DOMAIN, GeniusZone
STATE_AUTO = "auto" STATE_AUTO = "auto"
STATE_MANUAL = "manual" STATE_MANUAL = "manual"
GH_HEATERS = ["hot water temperature"] # Genius Hub HW zones support only Off, Override/Boost & Timer modes
GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
# HA does not have SUPPORT_ON_OFF for water_heater
GH_MAX_TEMP = 80.0
GH_MIN_TEMP = 30.0
# Genius Hub HW supports only Off, Override/Boost & Timer modes
HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"} HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"}
GH_STATE_TO_HA = { GH_STATE_TO_HA = {
"off": STATE_OFF, "off": STATE_OFF,
@ -34,91 +27,49 @@ GH_STATE_TO_HA = {
"linked": None, "linked": None,
"other": None, "other": None,
} }
GH_STATE_ATTRS = ["type", "override"]
GH_HEATERS = ["hot water temperature"]
async def async_setup_platform( async def async_setup_platform(
hass, hass_config, async_add_entities, discovery_info=None hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None
): ) -> None:
"""Set up the Genius Hub water_heater entities.""" """Set up the Genius Hub water_heater entities."""
client = hass.data[DOMAIN]["client"] if discovery_info is None:
return
entities = [ broker = hass.data[DOMAIN]["broker"]
GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS
]
async_add_entities(entities) async_add_entities(
[
GeniusWaterHeater(broker, z)
for z in broker.client.zone_objs
if z.data["type"] in GH_HEATERS
]
)
class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): class GeniusWaterHeater(GeniusZone, WaterHeaterDevice):
"""Representation of a Genius Hub water_heater device.""" """Representation of a Genius Hub water_heater device."""
def __init__(self, boiler) -> None: def __init__(self, broker, zone) -> None:
"""Initialize the water_heater device.""" """Initialize the water_heater device."""
super().__init__() super().__init__(broker, zone)
self._boiler = boiler self._max_temp = 80.0
self._operation_list = list(HA_OPMODE_TO_GH) self._min_temp = 30.0
self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
@property
def name(self) -> str:
"""Return the name of the water_heater device."""
return self._boiler.name
@property
def device_state_attributes(self) -> Dict[str, Any]:
"""Return the device state attributes."""
return {
"status": {
k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS
}
}
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return self._boiler.data.get("temperature")
@property
def target_temperature(self) -> float:
"""Return the temperature we try to reach."""
return self._boiler.data["setpoint"]
@property
def min_temp(self) -> float:
"""Return max valid temperature that can be set."""
return GH_MIN_TEMP
@property
def max_temp(self) -> float:
"""Return max valid temperature that can be set."""
return GH_MAX_TEMP
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return GH_SUPPORT_FLAGS
@property @property
def operation_list(self) -> List[str]: def operation_list(self) -> List[str]:
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
return self._operation_list return list(HA_OPMODE_TO_GH)
@property @property
def current_operation(self) -> str: def current_operation(self) -> str:
"""Return the current operation mode.""" """Return the current operation mode."""
return GH_STATE_TO_HA[self._boiler.data["mode"]] return GH_STATE_TO_HA[self._zone.data["mode"]]
async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: async def async_set_operation_mode(self, operation_mode) -> None:
"""Set a new operation mode for this boiler.""" """Set a new operation mode for this boiler."""
await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode])
async def async_set_temperature(self, **kwargs) -> Awaitable[None]:
"""Set a new target temperature for this boiler."""
temperature = kwargs[ATTR_TEMPERATURE]
await self._boiler.set_override(temperature, 3600) # 1 hour

View file

@ -525,7 +525,7 @@ gearbest_parser==1.0.7
geizhals==0.0.9 geizhals==0.0.9
# homeassistant.components.geniushub # homeassistant.components.geniushub
geniushub-client==0.6.13 geniushub-client==0.6.26
# homeassistant.components.geo_json_events # homeassistant.components.geo_json_events
# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.nsw_rural_fire_service_feed