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:
parent
c7da781efc
commit
c78b3a4439
7 changed files with 283 additions and 255 deletions
|
@ -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)
|
||||||
|
)
|
||||||
|
|
|
@ -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}
|
|
||||||
|
|
|
@ -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(
|
||||||
|
[
|
||||||
|
GeniusClimateZone(broker, z)
|
||||||
|
for z in broker.client.zone_objs
|
||||||
|
if z.data["type"] in GH_ZONES
|
||||||
]
|
]
|
||||||
async_add_entities(entities)
|
)
|
||||||
|
|
||||||
|
|
||||||
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"))
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
values = self._device._raw["childValues"] # pylint: disable=protected-access
|
interval = timedelta(
|
||||||
|
seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60)
|
||||||
last_comms = utc_from_timestamp(values["lastComms"]["val"])
|
)
|
||||||
if "WakeUp_Interval" in values:
|
if self._last_comms < dt_util.utcnow() - interval * 3:
|
||||||
interval = timedelta(seconds=values["WakeUp_Interval"]["val"])
|
|
||||||
else:
|
|
||||||
interval = timedelta(minutes=20)
|
|
||||||
|
|
||||||
if last_comms < utcnow() - interval * 3:
|
|
||||||
return "mdi:battery-unknown"
|
return "mdi:battery-unknown"
|
||||||
|
|
||||||
battery_level = self._device.data["state"]["batteryLevel"]
|
battery_level = self._device.data["state"][self._state_attr]
|
||||||
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
|
||||||
|
|
|
@ -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(
|
||||||
|
[
|
||||||
|
GeniusWaterHeater(broker, z)
|
||||||
|
for z in broker.client.zone_objs
|
||||||
|
if z.data["type"] in GH_HEATERS
|
||||||
]
|
]
|
||||||
|
)
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue