Skip to content

Commit

Permalink
Tweak geniushub and bump client to v0.6.26 (#26640)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zxdavb committed Oct 2, 2019
1 parent c7da781 commit c78b3a4
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 256 deletions.
198 changes: 167 additions & 31 deletions homeassistant/components/geniushub/__init__.py
@@ -1,14 +1,22 @@
"""Support for a Genius Hub system."""
from datetime import timedelta
import logging
from typing import Awaitable
from typing import Any, Dict, Optional

import aiohttp
import voluptuous as vol

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.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
Expand All @@ -19,82 +27,106 @@
)
from homeassistant.helpers.entity import Entity
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__)

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)

_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string})
_V3_API_SCHEMA = vol.Schema(
MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$"

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_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP),
}
)
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."""
kwargs = dict(hass_config[DOMAIN])
hass.data[DOMAIN] = {}

kwargs = dict(config[DOMAIN])
if CONF_HOST in kwargs:
args = (kwargs.pop(CONF_HOST),)
else:
args = (kwargs.pop(CONF_TOKEN),)
hub_uid = kwargs.pop(CONF_MAC, None)

hass.data[DOMAIN] = {}
broker = GeniusBroker(hass, args, kwargs)
client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass))

broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid)

try:
await broker.client.update()
await client.update()
except aiohttp.ClientResponseError as err:
_LOGGER.error("Setup failed, check your configuration, %s", err)
return False
broker.make_debug_log_entries()

async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL)

for platform in ["climate", "water_heater"]:
hass.async_create_task(
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)
)
for platform in ["climate", "water_heater", "sensor", "binary_sensor"]:
hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config))

return True


class GeniusBroker:
"""Container for geniushub client and data."""

def __init__(self, hass, args, kwargs):
def __init__(self, hass, client, hub_uid) -> None:
"""Initialize the geniushub client."""
self.hass = hass
self.client = hass.data[DOMAIN]["client"] = GeniusHub(
*args, **kwargs, session=async_get_clientsession(hass)
)
self.client = client
self._hub_uid = hub_uid

@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):
async def async_update(self, now, **kwargs) -> None:
"""Update the geniushub client's data."""
try:
await self.client.update()
except aiohttp.ClientResponseError as err:
_LOGGER.warning("Update failed, %s", err)
_LOGGER.warning("Update failed, message is: %s", err)
return
self.make_debug_log_entries()

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."""
# pylint: disable=protected-access
_LOGGER.debug(
Expand All @@ -105,20 +137,25 @@ def make_debug_log_entries(self):


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."""
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."""
async_dispatcher_connect(self.hass, DOMAIN, self._refresh)

@callback
def _refresh(self) -> None:
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
def name(self) -> str:
"""Return the name of the geniushub entity."""
Expand All @@ -128,3 +165,102 @@ def name(self) -> str:
def should_poll(self) -> bool:
"""Return False as geniushub entities should not be polled."""
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)
)
49 changes: 21 additions & 28 deletions homeassistant/components/geniushub/binary_sensor.py
@@ -1,52 +1,45 @@
"""Support for Genius Hub binary_sensor devices."""
from typing import Any, Dict

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."""
client = hass.data[DOMAIN]["client"]
if discovery_info is None:
return

broker = hass.data[DOMAIN]["broker"]

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."""

def __init__(self, device) -> None:
def __init__(self, broker, device, state_attr) -> None:
"""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":
self._name = f"Dual Channel Receiver {device.id}"
self._name = f"{device.type[:21]} {device.id}"
else:
self._name = f"{device.type} {device.id}"

@property
def is_on(self) -> bool:
"""Return the status of the sensor."""
return self._device.data["state"]["outputOnOff"]

@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}
return self._device.data["state"][self._state_attr]

0 comments on commit c78b3a4

Please sign in to comment.