Skip to content

Commit

Permalink
Structures for receiving hubitat events (#287)
Browse files Browse the repository at this point in the history
Structures for receiving hubitat events
  • Loading branch information
anschweitzer committed Apr 22, 2024
1 parent 627a9d7 commit fe5eef8
Show file tree
Hide file tree
Showing 24 changed files with 292 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "gridworks-protocol"
version = "0.7.2"
version = "0.7.3"
description = "Gridworks Protocol"
authors = ["Jessica Millar <jmillar@gridworks-consulting.com>"]
license = "MIT"
Expand Down
4 changes: 4 additions & 0 deletions src/gwproto/data_classes/cacs/web_server_cac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass


class WebServerCac(ComponentAttributeClass): ...
5 changes: 5 additions & 0 deletions src/gwproto/data_classes/components/hubitat_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

class HubitatComponent(Component):
hubitat_gt: HubitatGt
web_listener_nodes: set[str]

def __init__(
self,
Expand All @@ -18,6 +19,7 @@ def __init__(
hw_uid: Optional[str] = None,
):
self.hubitat_gt = hubitat_gt
self.web_listener_nodes = set()
super().__init__(
component_id=component_id,
component_attribute_class_id=component_attribute_class_id,
Expand All @@ -27,3 +29,6 @@ def __init__(

def urls(self) -> dict[str, Optional[yarl.URL]]:
return self.hubitat_gt.urls()

def add_web_listener(self, web_listener_node: str) -> None:
self.web_listener_nodes.add(web_listener_node)
14 changes: 12 additions & 2 deletions src/gwproto/data_classes/components/hubitat_poller_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def rest(self) -> RESTPollerSettings:

def resolve(
self,
tank_node_name: str,
node_name: str,
nodes: dict[str, ShNode],
components: dict[str, Component],
):
Expand All @@ -56,10 +56,11 @@ def resolve(

# replace proxy hubitat component, which only had component id.
# with the actual hubitat component containing data.
self.hubitat_gt = HubitatComponentGt.from_component_id(
hubitat_component = HubitatComponentGt.from_component_id(
self.hubitat_gt.ComponentId,
components,
)
self.hubitat_gt = HubitatComponentGt.from_data_class(hubitat_component)

# Constuct url config on top of maker api url url config
self._rest = RESTPollerSettings(
Expand All @@ -69,6 +70,15 @@ def resolve(
poll_period_seconds=self.poller_gt.poll_period_seconds,
)

# register attributes which accept web posts
if (
self.poller_gt.web_listen_enabled
and hubitat_component.hubitat_gt.WebListenEnabled
):
for attribute in self.poller_gt.attributes:
if attribute.web_listen_enabled:
hubitat_component.add_web_listener(node_name)

def urls(self) -> dict[str, Optional[yarl.URL]]:
urls = self.hubitat_gt.urls()
for attribute in self.poller_gt.attributes:
Expand Down
11 changes: 10 additions & 1 deletion src/gwproto/data_classes/components/hubitat_tank_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class HubitatTankComponent(Component, ComponentResolver):
default_poll_period_seconds: Optional[float] = None
devices_gt: list[FibaroTempSensorSettingsGt]
devices: list[FibaroTempSensorSettings] = []
web_listen_enabled: bool

def __init__(
self,
Expand All @@ -36,6 +37,7 @@ def __init__(
self.sensor_supply_voltage = tank_gt.sensor_supply_voltage
self.default_poll_period_seconds = tank_gt.default_poll_period_seconds
self.devices_gt = list(tank_gt.devices)
self.web_listen_enabled = tank_gt.web_listen_enabled
super().__init__(
display_name=display_name,
component_id=component_id,
Expand All @@ -49,9 +51,10 @@ def resolve(
nodes: dict[str, ShNode],
components: dict[str, Component],
):
hubitat_component_gt = HubitatComponentGt.from_component_id(
hubitat_component = HubitatComponentGt.from_component_id(
self.hubitat.ComponentId, components
)
hubitat_component_gt = HubitatComponentGt.from_data_class(hubitat_component)
hubitat_settings = HubitatRESTResolutionSettings(hubitat_component_gt)
devices = [
FibaroTempSensorSettings.create(
Expand All @@ -73,6 +76,12 @@ def resolve(
self.hubitat = hubitat_component_gt
self.devices = devices

# register voltage attribute for fibaros which accept web posts
if self.web_listen_enabled and hubitat_component.hubitat_gt.WebListenEnabled:
for device in self.devices:
if device.web_listen_enabled:
hubitat_component.add_web_listener(tank_node_name)

def urls(self) -> dict[str, Optional[yarl.URL]]:
urls = self.hubitat.urls()
for device in self.devices:
Expand Down
24 changes: 24 additions & 0 deletions src/gwproto/data_classes/components/web_server_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Optional

from gwproto.data_classes.component import Component
from gwproto.types.web_server_gt import WebServerGt


class WebServerComponent(Component):
web_server_gt: WebServerGt

def __init__(
self,
component_id: str,
component_attribute_class_id: str,
web_server_gt: WebServerGt,
display_name: Optional[str] = None,
hw_uid: Optional[str] = None,
):
self.web_server_gt = web_server_gt
super().__init__(
component_id=component_id,
component_attribute_class_id=component_attribute_class_id,
display_name=display_name,
hw_uid=hw_uid,
)
44 changes: 40 additions & 4 deletions src/gwproto/data_classes/hardware_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import json
import re
import typing
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any
from typing import List
from typing import Optional
from typing import Type
from typing import TypeVar

from gwproto.data_classes.cacs.electric_meter_cac import ElectricMeterCac
from gwproto.data_classes.component import Component
Expand Down Expand Up @@ -50,6 +53,8 @@
)


T = TypeVar("T")

snake_add_underscore_to_camel_pattern = re.compile(r"(?<!^)(?=[A-Z])")


Expand Down Expand Up @@ -210,7 +215,9 @@ class HardwareLayout:
layout: dict[Any, Any]
cacs: dict[str, ComponentAttributeClass]
components: dict[str, Component]
components_by_type: dict[Type, list[Component]]
nodes: dict[str, ShNode]
nodes_by_component: dict[str, str]

def __init__(
self,
Expand All @@ -226,9 +233,15 @@ def __init__(
if components is None:
components = Component.by_id
self.components = dict(components)
self.components_by_type = defaultdict(list)
for component in components.values():
self.components_by_type[type(component)].append(component)
if nodes is None:
nodes = ShNode.by_id
self.nodes = dict(nodes)
self.nodes_by_component = {
node.component_id: node.alias for node in self.nodes.values()
}

def clear_property_cache(self) -> None:
for cached_prop_name in [
Expand Down Expand Up @@ -302,11 +315,33 @@ def load_dict(
def node(self, alias: str, default: Any = None) -> ShNode:
return self.nodes.get(alias, default)

def component(self, alias: str) -> Optional[Component]:
return self.component_from_node(self.node(alias, None))
def component(self, node_alias: str) -> Optional[Component]:
return self.component_from_node(self.node(node_alias, None))

def cac(self, node_alias: str) -> Optional[ComponentAttributeClass]:
return self.cac_from_component(self.component(node_alias))

def get_component_as_type(self, component_id: str, type_: Type[T]) -> Optional[T]:
component = self.components.get(component_id, None)
if component is not None and not isinstance(component, type_):
raise ValueError(
f"ERROR. Component <{component_id}> has type {type(component)} not {type_}"
)
return component

def get_components_by_type(self, type_: Type[T]) -> list[T]:
entries = self.components_by_type.get(type_, [])
for i, entry in enumerate(entries):
if not isinstance(entry, type_):
raise ValueError(
f"ERROR. Entry {i+1} in "
f"HardwareLayout.components_by_typ[{type_}] "
f"has the wrong type {type(entry)}"
)
return entries

def cac(self, alias: str) -> Optional[ComponentAttributeClass]:
return self.cac_from_component(self.component(alias))
def node_from_component(self, component_id: str) -> Optional[ShNode]:
return self.nodes.get(self.nodes_by_component.get(component_id, ""), None)

def component_from_node(self, node: Optional[ShNode]) -> Optional[Component]:
return (
Expand Down Expand Up @@ -497,6 +532,7 @@ def all_multipurpose_telemetry_tuples(self) -> List[TelemetryTuple]:
x.actor_class == ActorClass.MultipurposeSensor
or x.actor_class == ActorClass.HubitatTankModule
or x.actor_class == ActorClass.HubitatPoller
or x.actor_class == ActorClass.HoneywellThermostat
)
and hasattr(x.component, "config_list")
),
Expand Down
4 changes: 4 additions & 0 deletions src/gwproto/default_decoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import gwproto.types.hubitat_tank_component_gt # noqa
import gwproto.types.rest_poller_cac_gt # noqa
import gwproto.types.rest_poller_component_gt # noqa
import gwproto.types.web_server_cac_gt # noqa
import gwproto.types.web_server_component_gt # noqa
from gwproto.data_classes.component import Component
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass
from gwproto.decoders import PydanticTypeNameDecoder
Expand Down Expand Up @@ -86,6 +88,7 @@ def decode_to_data_class(
"gwproto.types.hubitat_poller_cac_gt",
"gwproto.types.hubitat_tank_cac_gt",
"gwproto.types.rest_poller_cac_gt",
"gwproto.types.web_server_cac_gt",
],
)

Expand All @@ -97,5 +100,6 @@ def decode_to_data_class(
"gwproto.types.hubitat_poller_component_gt",
"gwproto.types.hubitat_tank_component_gt",
"gwproto.types.rest_poller_component_gt",
"gwproto.types.web_server_component_gt",
],
)
12 changes: 12 additions & 0 deletions src/gwproto/enums/actor_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class ActorClass(StrEnum):
thermal storage tank. [More Info](https://drive.google.com/drive/u/0/folders/1GSxDd8Naf1GKK_fSOgQU933M1UcJ4r8q).
- HubitatPoller (00000100): An actor for representing a somewhat generic ShNode (like a thermostat)
that can be polled through the Hubitat.
- Hubitat: (0000101): An actor for representing a Hubitat for receiving Hubitat events over HTTP.
- HoneywellThermostat: (0000102): An actor for representing a Honeywell Hubitat thermostat which
can load thermostat heating state change messages into status reports.
"""

NoActor = auto()
Expand All @@ -67,6 +71,8 @@ class ActorClass(StrEnum):
HubitatTelemetryReader = auto()
HubitatTankModule = auto()
HubitatPoller = auto()
Hubitat = auto()
HoneywellThermostat = auto()

@classmethod
def default(cls) -> "ActorClass":
Expand Down Expand Up @@ -172,6 +178,8 @@ def symbols(cls) -> List[str]:
"0401b27e",
"e2877329",
"00000100",
"00000101",
"00000102",
]


Expand All @@ -188,6 +196,8 @@ def symbols(cls) -> List[str]:
"0401b27e": "HubitatTelemetryReader",
"e2877329": "HubitatTankModule",
"00000100": "HubitatPoller",
"00000101": "Hubitat",
"00000102": "HoneywellThermostat",
}

value_to_symbol = {value: key for key, value in symbol_to_value.items()}
Expand All @@ -205,4 +215,6 @@ def symbols(cls) -> List[str]:
"HubitatTelemetryReader": "001",
"HubitatTankModule": "001",
"HubitatPoller": "001",
"Hubitat": "001",
"HoneywellThermostat": "001",
}
5 changes: 5 additions & 0 deletions src/gwproto/enums/telemetry_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TelemetryName(StrEnum):
6234 means 6.234 deg Celcius.
- AirTempFTimes1000 (4c3f8c78): Air temperature, in Degrees F multiplied by 1000. Example:
69329 means 69.329 deg Fahrenheit.
- ThermostatState (00002000): An enum representing the state of the thermostat heat call.
"""

Unknown = auto()
Expand All @@ -56,6 +57,7 @@ class TelemetryName(StrEnum):
FrequencyMicroHz = auto()
AirTempCTimes1000 = auto()
AirTempFTimes1000 = auto()
ThermostatState = auto()

@classmethod
def default(cls) -> "TelemetryName":
Expand Down Expand Up @@ -162,6 +164,7 @@ def symbols(cls) -> List[str]:
"337b8659",
"0f627faa",
"4c3f8c78",
"00002000",
]


Expand All @@ -179,6 +182,7 @@ def symbols(cls) -> List[str]:
"337b8659": "FrequencyMicroHz",
"0f627faa": "AirTempCTimes1000",
"4c3f8c78": "AirTempFTimes1000",
"00002000": "ThermostatState",
}

value_to_symbol = {value: key for key, value in symbol_to_value.items()}
Expand All @@ -197,4 +201,5 @@ def symbols(cls) -> List[str]:
"FrequencyMicroHz": "001",
"AirTempCTimes1000": "001",
"AirTempFTimes1000": "001",
"ThermostatState": "001",
}
5 changes: 5 additions & 0 deletions src/gwproto/enums/unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Unit(StrEnum):
- AmpsRms (a969ac7c)
- VoltsRms (e5d7555c)
- Gallons (8e123a26)
- ThermostatStateEnum (00003000)
"""

Unknown = auto()
Expand All @@ -38,6 +39,7 @@ class Unit(StrEnum):
AmpsRms = auto()
VoltsRms = auto()
Gallons = auto()
ThermostatStateEnum = auto()

@classmethod
def default(cls) -> "Unit":
Expand Down Expand Up @@ -141,6 +143,7 @@ def symbols(cls) -> List[str]:
"a969ac7c",
"e5d7555c",
"8e123a26",
"00003000",
]


Expand All @@ -155,6 +158,7 @@ def symbols(cls) -> List[str]:
"a969ac7c": "AmpsRms",
"e5d7555c": "VoltsRms",
"8e123a26": "Gallons",
"00003000": "ThermostatStateEnum",
}

value_to_symbol = {value: key for key, value in symbol_to_value.items()}
Expand All @@ -170,4 +174,5 @@ def symbols(cls) -> List[str]:
"AmpsRms": "000",
"VoltsRms": "000",
"Gallons": "000",
"ThermostatStateEnum": "000"
}

0 comments on commit fe5eef8

Please sign in to comment.