Skip to content

Commit

Permalink
Create hub ID, use in events
Browse files Browse the repository at this point in the history
Use the first 8 characters of the hub access token as the hub ID. Use
this in the config entry title (update existing titles) and as a device
identifier, and and include it in hubitat_events.

resolves #50
  • Loading branch information
jason0x43 committed Jun 14, 2020
1 parent 84617ee commit 763d2ad
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 36 deletions.
6 changes: 6 additions & 0 deletions custom_components/hubitat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""The Hubitat integration."""
from asyncio import gather
from logging import getLogger
import re
from typing import Any, Dict

import voluptuous as vol
Expand Down Expand Up @@ -41,6 +42,11 @@ def stop_hub(event: Event) -> None:

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_hub)

# If this config entry's title uses a MAC address, rename it to use the hub
# ID
if re.match(r"Hubitat \(\w{2}(:\w{2}){5}\)", entry.title):
hass.config_entries.async_update_entry(entry, title=f"Hubitat ({hub.id})")

hass.bus.fire(CONF_HUBITAT_EVENT, {"name": "ready"})
_LOGGER.info("Hubitat is ready")

Expand Down
5 changes: 2 additions & 3 deletions custom_components/hubitat/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from homeassistant.core import callback

from .const import CONF_APP_ID, CONF_SERVER_PORT, DOMAIN, TEMP_C, TEMP_F
from .util import get_hub_short_id

_LOGGER = logging.getLogger(__name__)

Expand All @@ -47,9 +48,7 @@ async def validate_input(data: Dict[str, Any]) -> Dict[str, Any]:
hub = HubitatHub(host, app_id, token)
await hub.check_config()

return {
"label": f"Hubitat ({hub.mac})",
}
return {"label": f"Hubitat ({get_hub_short_id(hub)})"}


class HubitatConfigFlow(ConfigFlow, domain=DOMAIN): # type: ignore
Expand Down
1 change: 1 addition & 0 deletions custom_components/hubitat/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
ATTR_DOUBLE_TAPPED = "double_tapped"
ATTR_ENTRY_DELAY = "entry_delay"
ATTR_EXIT_DELAY = "exit_delay"
ATTR_HUB = "hub"
ATTR_LAST_CODE_NAME = "last_code_name"
ATTR_LENGTH = "length"
ATTR_MAX_CODES = "max_codes"
Expand Down
34 changes: 13 additions & 21 deletions custom_components/hubitat/device.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Classes for managing Hubitat devices."""

from hashlib import sha256
from json import loads
from logging import getLogger
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union, cast
Expand All @@ -22,6 +21,7 @@

from .const import (
ATTR_ATTRIBUTE,
ATTR_HUB,
CONF_APP_ID,
CONF_HUBITAT_EVENT,
CONF_SERVER_PORT,
Expand All @@ -30,6 +30,7 @@
TEMP_F,
TRIGGER_CAPABILITIES,
)
from .util import get_hub_short_id, get_token_hash

# Hubitat attributes that should be emitted as HA events
_TRIGGER_ATTRS = tuple([v.attr for v in TRIGGER_CAPABILITIES.values()])
Expand Down Expand Up @@ -98,6 +99,11 @@ def host(self) -> str:
),
)

@property
def id(self) -> str:
"""A unique ID for this hub instance."""
return get_hub_short_id(self._hub)

@property
def mac(self) -> Optional[str]:
"""The MAC address of the associated Hubitat hub."""
Expand All @@ -113,16 +119,6 @@ def token(self) -> str:
"""The token used to access the Maker API."""
return cast(str, self.config_entry.data.get(CONF_ACCESS_TOKEN))

@property
def token_hash(self) -> str:
"""The token used to access the Maker API."""
if not hasattr(self, "_token_hash"):
token = self.config_entry.data[CONF_ACCESS_TOKEN]
hasher = sha256()
hasher.update(token.encode("utf-8"))
self._token_hash = hasher.hexdigest()
return self._token_hash

@property
def temperature_unit(self) -> str:
"""The units used for temperature values."""
Expand Down Expand Up @@ -200,7 +196,7 @@ async def async_update_device_registry(self) -> None:
dreg.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
connections={(device_registry.CONNECTION_NETWORK_MAC, self._hub.mac)},
identifiers={(DOMAIN, self._hub.mac)},
identifiers={(DOMAIN, self.id)},
manufacturer="Hubitat",
name="Hubitat Elevation",
)
Expand Down Expand Up @@ -274,7 +270,7 @@ def __init__(self, hub: Hub, device: Device) -> None:
"""Initialize a device."""
self._hub = hub
self._device: Device = device
self._id = f"{self._hub.token_hash}::{self._device.id}"
self._id = f"{get_token_hash(hub.token)}::{self._device.id}"
self._old_ids = [
f"{self._hub.host}::{self._hub.app_id}::{self._device.id}",
f"{self._hub.mac}::{self._hub.app_id}::{self._device.id}",
Expand All @@ -294,7 +290,7 @@ def device_info(self) -> Dict[str, Any]:
"name": self._device.name,
"manufacturer": "Hubitat",
"model": self.type,
"via_device": (DOMAIN, self._hub.mac),
"via_device": (DOMAIN, self._hub.id),
}

@property
Expand Down Expand Up @@ -363,7 +359,9 @@ def get_str_attr(self, attr: str) -> Optional[str]:
def handle_event(self, event: Event) -> None:
"""Handle an event received from the Hubitat hub."""
if event.attribute in _TRIGGER_ATTRS:
evt = to_event_dict(event)
evt = dict(event)
evt[ATTR_ATTRIBUTE] = _TRIGGER_ATTR_MAP[event.attribute]
evt[ATTR_HUB] = self._hub.id
self._hub.hass.bus.async_fire(CONF_HUBITAT_EVENT, evt)
_LOGGER.debug("Emitted event %s", evt)

Expand Down Expand Up @@ -423,9 +421,3 @@ def get_hub(hass: HomeAssistant, entry_id: str) -> Hub:
"""Get the Hub device associated with a given config entry."""
hub: Hub = hass.data[DOMAIN][entry_id]
return hub


def to_event_dict(event: Event) -> Dict[str, Any]:
evt = dict(event)
evt[ATTR_ATTRIBUTE] = _TRIGGER_ATTR_MAP[event.attribute]
return evt
30 changes: 19 additions & 11 deletions custom_components/hubitat/device_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from itertools import chain
from json import loads
import logging
from typing import Any, Callable, Dict, List, Optional, Sequence, cast
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, cast

from hubitatmaker import (
ATTR_DEVICE_ID,
Expand All @@ -29,6 +29,7 @@

from .const import (
ATTR_ATTRIBUTE,
ATTR_HUB,
CONF_BUTTONS,
CONF_DOUBLE_TAPPED,
CONF_HELD,
Expand All @@ -39,7 +40,7 @@
DOMAIN,
TRIGGER_CAPABILITIES,
)
from .device import get_hub
from .device import Hub, get_hub

TRIGGER_TYPES = tuple([v.conf for v in TRIGGER_CAPABILITIES.values()])
TRIGGER_SUBTYPES = set(
Expand Down Expand Up @@ -67,7 +68,7 @@ async def async_validate_trigger_config(
raise InvalidDeviceAutomationConfig

if DOMAIN in hass.config.components:
hubitat_device = await get_hubitat_device(hass, device.id)
hubitat_device, _ = await get_hubitat_device(hass, device.id)
if hubitat_device is None:
_LOGGER.warning("Invalid Hubitat device")
raise InvalidDeviceAutomationConfig
Expand All @@ -91,7 +92,7 @@ async def async_get_triggers(
hass: HomeAssistant, device_id: str
) -> Sequence[Dict[str, Any]]:
"""List device triggers for Hubitat devices."""
device = await get_hubitat_device(hass, device_id)
device, _ = await get_hubitat_device(hass, device_id)
if device is None:
return []

Expand Down Expand Up @@ -133,17 +134,22 @@ async def async_attach_trigger(
automation_info: Dict[str, Any],
) -> Callable[[], None]:
"""Attach a trigger."""
hubitat_device = await get_hubitat_device(hass, config[CONF_DEVICE_ID])
if hubitat_device is None:
result = await get_hubitat_device(hass, config[CONF_DEVICE_ID])

if result[0] is None or result[1] is None:
_LOGGER.warning(
"Could not find Hubitat device for ID %s", config[CONF_DEVICE_ID]
)
raise InvalidDeviceAutomationConfig

hubitat_device: Device = result[0]
hub: Hub = result[1]

# Event data should match up to the data a hubitat_event event would
# contain
event_data = {
ATTR_DEVICE_ID: hubitat_device.id,
ATTR_HUB: hub.id,
ATTR_ATTRIBUTE: config[CONF_TYPE],
}
if CONF_SUBTYPE in config:
Expand All @@ -170,11 +176,13 @@ async def get_device(hass: HomeAssistant, device_id: str) -> Optional[DeviceEntr
return device_registry.async_get(device_id)


async def get_hubitat_device(hass: HomeAssistant, device_id: str) -> Optional[Device]:
async def get_hubitat_device(
hass: HomeAssistant, device_id: str
) -> Tuple[Optional[Device], Optional[Hub]]:
"""Return a Hubitat device for a given Home Assistant device ID."""
device = await get_device(hass, device_id)
if device is None:
return None
return None, None

hubitat_id = None
for identifier in device.identifiers:
Expand All @@ -184,15 +192,15 @@ async def get_hubitat_device(hass: HomeAssistant, device_id: str) -> Optional[De

if hubitat_id is None:
_LOGGER.debug("Couldn't find Hubitat ID for device %s", device_id)
return None
return None, None

for entry_id in device.config_entries:
hub = get_hub(hass, entry_id)
if hubitat_id in hub.devices:
return hub.devices[hubitat_id]
return hub.devices[hubitat_id], hub

_LOGGER.debug("Couldn't find Hubitat device for ID %s", hubitat_id)
return None
return None, None


def get_trigger_types(device: Device) -> Sequence[str]:
Expand Down
17 changes: 17 additions & 0 deletions custom_components/hubitat/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from hashlib import sha256

from hubitatmaker import Hub

_token_hashes = {}


def get_token_hash(token: str) -> str:
if token not in _token_hashes:
hasher = sha256()
hasher.update(token.encode("utf-8"))
_token_hashes[token] = hasher.hexdigest()
return _token_hashes[token]


def get_hub_short_id(hub: Hub) -> str:
return hub.token[:8]
13 changes: 12 additions & 1 deletion stubs/homeassistant/config_entries.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Coroutine, Dict, Mapping, Optional
from typing import Any, Callable, Coroutine, Dict, Mapping, Optional, Union

import voluptuous as vol

Expand All @@ -10,6 +10,7 @@ class ConfigEntry:
options: Mapping[str, Any]
data: Dict[str, Any]
entry_id: str
title: str
def add_update_listener(
self,
listener: Callable[[HomeAssistant, ConfigEntry], Coroutine[Any, Any, None]],
Expand All @@ -22,6 +23,16 @@ class ConfigEntries:
async def async_forward_entry_unload(
self, entry: ConfigEntry, domain: str
) -> bool: ...
def async_update_entry(
self,
entry: ConfigEntry,
*,
unique_id: Union[str, Dict[str, Any], None] = ...,
title: Union[str, Dict[str, Any]] = ...,
data: Dict[str, Any] = ...,
options: Dict[str, Any] = ...,
system_options: Dict[str, Any] = ...,
) -> None: ...

class _FlowHandler:
def async_create_entry(
Expand Down

0 comments on commit 763d2ad

Please sign in to comment.