Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak geniushub and bump client to v0.6.26 #26640

Merged
merged 55 commits into from Oct 2, 2019
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
dadbeba
use state attribute rather than type
zxdavb Sep 12, 2019
3e41c71
HA style tweaks
zxdavb Sep 13, 2019
5b39ee3
small tweak
zxdavb Sep 13, 2019
d45e16e
bump client
zxdavb Sep 14, 2019
2dcfe12
Merge remote-tracking branch 'upstream/dev' into geniushub_tweak
zxdavb Sep 14, 2019
0c42f24
add more device_state_attributes
zxdavb Sep 16, 2019
ff5cf36
bump client
zxdavb Sep 17, 2019
0979c9c
small tweak
zxdavb Sep 18, 2019
2451080
bump client for concurrent IO
zxdavb Sep 19, 2019
84f51e1
force snake_case, and refactor (consolidate) Devices/Zones
zxdavb Sep 19, 2019
7c61e51
force snake_case, and refactor (consolidate) Devices/Zones 2
zxdavb Sep 19, 2019
96bc9ea
force snake_case, and refactor (consolidate) Devices/Zones 3
zxdavb Sep 19, 2019
e09d086
Merge remote-tracking branch 'upstream/dev' into geniushub_tweak
zxdavb Sep 19, 2019
743527a
refactor last_comms / wakeup_interval check
zxdavb Sep 19, 2019
84fba7d
movement sensor is dynamic, and tweaking
zxdavb Sep 20, 2019
f59d004
tweak
zxdavb Sep 20, 2019
caf6781
bump client to v0.6.20
zxdavb Sep 20, 2019
2f3a943
dummy
zxdavb Sep 20, 2019
549324e
dummy 2
zxdavb Sep 20, 2019
70f2d94
Merge remote-tracking branch 'upstream/dev' into geniushub_tweak
zxdavb Sep 20, 2019
212ebdb
bump client to handle another edge case
zxdavb Sep 20, 2019
72fc3a4
Merge remote-tracking branch 'upstream/dev' into geniushub_tweak
zxdavb Sep 21, 2019
ac700d4
use entity_id fro zones
zxdavb Sep 21, 2019
71572d6
small tweak
zxdavb Sep 21, 2019
a2ec709
bump client to 0.6.22
zxdavb Sep 21, 2019
c2f09a2
add recursive snake_case converter
zxdavb Sep 23, 2019
92dc3fe
fix regression
zxdavb Sep 23, 2019
95146b7
fix regression 2
zxdavb Sep 23, 2019
7c4d086
fix regression 3
zxdavb Sep 23, 2019
4423d5e
remove Awaitables
zxdavb Sep 23, 2019
e31653c
don't dynamically create function every scan_interval
zxdavb Sep 26, 2019
151491e
log kast_comms as localtime, delint dt_util
zxdavb Sep 26, 2019
67e50c0
add sensors fro v1 API
zxdavb Sep 26, 2019
8ed69c3
tweak entity_id
zxdavb Sep 28, 2019
ce18192
bump client
zxdavb Sep 29, 2019
cca2c8d
bump client to v0.6.24
zxdavb Sep 29, 2019
c2d9283
bump client to v0.6.25
zxdavb Sep 29, 2019
37f1f96
explicit device attrs, dt as UTC
zxdavb Oct 1, 2019
c4535df
Merge remote-tracking branch 'upstream/dev' into geniushub_tweak
zxdavb Oct 1, 2019
1d4a9c8
add unique_id, remove entity_id
zxdavb Oct 1, 2019
92532ee
Bump client to 0.6.26 - add Hub UID
zxdavb Oct 1, 2019
7527db1
remove convert_dict()
zxdavb Oct 1, 2019
d748ef3
add mac_address (uid) for v1 API
zxdavb Oct 2, 2019
5383f36
tweak var names
zxdavb Oct 2, 2019
13fcf7c
add UID.upper() to avoid unwanted unique_id changes
zxdavb Oct 2, 2019
10361b2
Update homeassistant/components/geniushub/__init__.py
zxdavb Oct 2, 2019
37b3396
Update homeassistant/components/geniushub/__init__.py
zxdavb Oct 2, 2019
e03814e
remove underscores
zxdavb Oct 2, 2019
1ba2479
Merge branch 'geniushub_tweak' of github.com:zxdavb/home-assistant in…
zxdavb Oct 2, 2019
82b2b3a
refactor for broker
zxdavb Oct 2, 2019
79f23d0
ready now
zxdavb Oct 2, 2019
9bdb7e2
validate UID (MAC address)
zxdavb Oct 2, 2019
a699ee4
move uid to broker
zxdavb Oct 2, 2019
9ada043
use existing constant
zxdavb Oct 2, 2019
67affb1
pass client to broker
zxdavb Oct 2, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
166 changes: 153 additions & 13 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
import re
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_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,11 +27,25 @@
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval
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",
zxdavb marked this conversation as resolved.
Show resolved Hide resolved
"measuredTemperature",
"occupancyTrigger",
"setback",
"setTemperature",
"wakeupInterval",
]

SCAN_INTERVAL = timedelta(seconds=60)

_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string})
Expand All @@ -39,6 +61,24 @@
)


def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]:
"""Recursively convert a dict's keys to snake_case."""

def convert_key(key: str) -> str:
"""Convert a string to snake_case."""
string = re.sub(r"[\-\.\s]", "_", str(key))
return (string[0]).lower() + re.sub(
r"[A-Z]", lambda matched: "_" + matched.group(0).lower(), string[1:]
)

return {
(convert_key(k) if isinstance(k, str) else k): (
convert_dict(v) if isinstance(v, dict) else v
)
for k, v in dictionary.items()
}


async def async_setup(hass, hass_config):
"""Create a Genius Hub system."""
kwargs = dict(hass_config[DOMAIN])
Expand All @@ -59,17 +99,11 @@ async def async_setup(hass, hass_config):

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(
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


Expand All @@ -88,7 +122,7 @@ async def async_update(self, now, **kwargs):
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()

Expand All @@ -105,20 +139,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):
"""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 +167,104 @@ 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, device):
"""Initialize the Device."""
super().__init__()

self._device = device
# pylint: disable=protected-access
self._unique_id = (
f"{device._hub.uid}_device_{device.id}" if device._hub.uid else None
)

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"] = {k: v for k, v in state.items() if k in GH_DEVICE_ATTRS}
zxdavb marked this conversation as resolved.
Show resolved Hide resolved

return convert_dict(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, zone) -> None:
"""Initialize the Zone."""
super().__init__()

self._zone = zone
# pylint: disable=protected-access
self._unique_id = f"{zone._hub.uid}_zone_{zone.id}" if zone._hub.uid else None
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

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)
)
42 changes: 16 additions & 26 deletions homeassistant/components/geniushub/binary_sensor.py
@@ -1,52 +1,42 @@
"""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 . 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):
"""Set up the Genius Hub sensor entities."""
if discovery_info is None:
return

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

switches = [
GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH
GeniusBinarySensor(d, GH_STATE_ATTR)
for d in 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, device, state_attr) -> None:
"""Initialize the binary sensor."""
super().__init__()
super().__init__(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}"
zxdavb marked this conversation as resolved.
Show resolved Hide resolved
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]