Skip to content

Commit

Permalink
Merge pull request #216 from thegridelectric/dev
Browse files Browse the repository at this point in the history
Tank Module via HTTP/Hubitat/Fibaro (#205)

This PR adds support for Tank modules accessed by sending HTTP polls to a Hubitat which is configured to communicate with Fibaro Smart Implant temperature sensors on the tank via Fibaro / Hubitat ZWave network. 

Changes: 
1. New CACS, Components and enums added for Fibaro Smart Implant, Hubitat, Hubitat Tank Module and generic RESTPoller. 
2. HardwareLayout has been modified: 
    1. Components and CACS in OtherComponents/OtherCACs can be decoded from TypeName without need to specify the decoding class. 
    2. A "resolve" phase has been added after loading CACs, Components and Nodes to allow validation of relationships between entities when that validation requires all CACs, Components and Nodes to have been decoded. 
3. Code generation has been broken by this merge. Fixing up code generation will be addressed by #214. More code generation discussion in #215.
  • Loading branch information
anschweitzer committed Oct 31, 2023
2 parents 5140fa2 + 353386c commit fc1aefc
Show file tree
Hide file tree
Showing 42 changed files with 9,239 additions and 4,836 deletions.
2 changes: 1 addition & 1 deletion CodeGenerationTools/GridworksCore/Enum/DeriveEnums.xslt
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class </xsl:text><xsl:value-of select="$local-class-name"/>
"""
</xsl:text>

<xsl:for-each select="$airtable//EnumSymbols/EnumSymbol[(Enum = $enum-id)]">
<xsl:for-each select="$airtable//EnumSymbols/EnumSymbol[(Enum = $enum-id) and (Version &lt;= $current-version)]">
<xsl:sort select="Idx" data-type="number"/>
<xsl:if test="$enum-name-style = 'Upper'">
<xsl:value-of select="translate(translate(LocalValue,'-',''),$lcletters, $ucletters)"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<xsl:with-param name="mp-schema-text" select="LocalName" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="current-version" select="Version" />
<FileSetFile>
<xsl:element name="RelativePath"><xsl:text>../../../../tests/enums/</xsl:text>
<xsl:value-of select="translate(LocalName,'.','_')"/><xsl:text>_test.py</xsl:text></xsl:element>
Expand All @@ -51,8 +52,8 @@ def test_</xsl:text> <xsl:value-of select="translate(LocalName,'.','_')"/>

assert set(</xsl:text><xsl:value-of select="$local-class-name"/><xsl:text>.values()) == {
</xsl:text>
<xsl:for-each select="$airtable//EnumSymbols/EnumSymbol[(Enum = $enum-id)]">
<xsl:sort select="Idx"/>
<xsl:for-each select="$airtable//EnumSymbols/EnumSymbol[(Enum = $enum-id) and (Version &lt;= $current-version)]">
<xsl:sort select="Idx" data-type="number"/>
<xsl:text>"</xsl:text>
<xsl:if test="$enum-name-style = 'Upper'">
<xsl:value-of select="translate(translate(LocalValue,'-',''),$lcletters, $ucletters)"/>
Expand Down
22 changes: 15 additions & 7 deletions CodeGenerationTools/GridworksCore/ODXML/DataSchema.odxml

Large diffs are not rendered by default.

11,802 changes: 7,069 additions & 4,733 deletions CodeGenerationTools/GridworksCore/SSoT/Airtable.xml

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions CodeGenerationTools/GridworksCore/SSoT/Entities.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"recsCmQ5g1sy3MKx6",
"recuhuEDQn3NKBJJf",
"rec2djRgncV4Fpy3U",
"recnMa5xJD7jb3ier"
"recnMa5xJD7jb3ier",
"recNZ1bh0Ysk48HHN"
],
"TableGroup": "General",
"ToName": "SchemaRoot"
Expand Down Expand Up @@ -275,7 +276,9 @@
"rec4UQz0uQGTOXP7v",
"recjBcWHaD0txkThT",
"recPChJ4Z0yYiZUOA",
"recCc4GQB5icU2hYH"
"recCc4GQB5icU2hYH",
"recnHjL5hWmhS57nY",
"recHpojco9I09N58x"
],
"TableGroup": "General",
"ToName": "Schema"
Expand Down
11 changes: 0 additions & 11 deletions CodeGenerationTools/GridworksCore/aicapture.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,6 @@
"RelativePath": "/Types/Docs/ApiDocs/Toc",
"CommandLine": "codee42/xml-xslt-transform -i ../../../../SSoT/Airtable.xml -i DeriveToc.xslt -i ../../../../GnfCommon.xslt -i ../../../../ODXML/DataSchema.odxml -w 120000"
},
{
"MatchedTranspiler": {
"TranspilerId": "4b713f03-1d9f-42f2-b02f-e3cdbd237bb3",
"Name": "XmlXsltTransform",
"Description": "Given an Xml and Xslt file, will process and \"split\" a resulting fileset."
},
"ProjectTranspilerId": "a2b6bc61-44d1-4bcd-b35c-fef614baf114",
"Name": "XmlXsltTransform",
"RelativePath": "/Types/ApiUtil",
"CommandLine": "codee42/xml-xslt-transform -i ../../SSoT/Airtable.xml -i DeriveApiUtil.xslt -i ../../GnfCommon.xslt -i ../../ODXML/DataSchema.odxml -w 120000"
},
{
"MatchedTranspiler": {
"TranspilerId": "4b713f03-1d9f-42f2-b02f-e3cdbd237bb3",
Expand Down
172 changes: 171 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 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.5.7"
version = "0.6.0"
description = "Gridworks Protocol"
authors = ["Jessica Millar <[email protected]>"]
license = "MIT"
Expand All @@ -24,6 +24,7 @@ pydantic = "^1.10.2"
pendulum = "^2.1.2"
fastapi-utils = "^0.2.1"
gridworks = "^0.2.9"
yarl = "^1.9.2"

[tool.poetry.dev-dependencies]
Pygments = ">=2.10.0"
Expand Down
24 changes: 19 additions & 5 deletions src/gwproto/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import gwproto.enums as enums
import gwproto.messages as messages
import gwproto.property_format as property_format
from gwproto.data_classes.hardware_layout import HardwareLayout
from gwproto.data_classes.sh_node import ShNode
from gwproto.decoders import CallableDecoder
from gwproto.decoders import Decoder
from gwproto.decoders import DecoderItem
Expand All @@ -11,9 +10,16 @@
from gwproto.decoders import MQTTCodec
from gwproto.decoders import OneDecoderExtractor
from gwproto.decoders import PydanticDecoder
from gwproto.decoders import PydanticTypeNameDecoder
from gwproto.decoders import create_discriminator
from gwproto.decoders import create_message_payload_discriminator
from gwproto.decoders import get_pydantic_literal_type_name
from gwproto.decoders import pydantic_named_types
from gwproto.default_decoders import CacDecoder
from gwproto.default_decoders import ComponentDecoder
from gwproto.default_decoders import decode_to_data_class
from gwproto.default_decoders import default_cac_decoder
from gwproto.default_decoders import default_component_decoder
from gwproto.errors import SchemaError
from gwproto.message import Header
from gwproto.message import Message
Expand All @@ -24,25 +30,33 @@

__all__ = [
"as_enum",
"CacDecoder",
"CallableDecoder",
"ComponentDecoder",
"create_discriminator",
"create_message_payload_discriminator",
"decode_to_data_class",
"Decoder",
"DecoderItem",
"DecodedMQTTTopic",
"Decoders",
"enums",
"default_cac_decoder",
"default_component_decoder",
"get_pydantic_literal_type_name",
"HardwareLayout",
"Header",
"MakerDecoder",
"MakerExtractor",
"Message",
"messages",
"MessageDiscriminator",
"SchemaError",
"MQTTCodec",
"MQTTTopic",
"OneDecoderExtractor",
"property_format",
"PydanticDecoder",
"PydanticTypeNameDecoder",
"pydantic_named_types",
"SchemaError",
"ShNode",
]
5 changes: 5 additions & 0 deletions src/gwproto/data_classes/cacs/fibaro_smart_implant_cac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass


class FibaroSmartImplantCac(ComponentAttributeClass):
...
5 changes: 5 additions & 0 deletions src/gwproto/data_classes/cacs/hubitat_cac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass


class HubitatCac(ComponentAttributeClass):
...
5 changes: 5 additions & 0 deletions src/gwproto/data_classes/cacs/hubitat_tank_module_cac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass


class HubitatTankModuleCac(ComponentAttributeClass):
...
5 changes: 5 additions & 0 deletions src/gwproto/data_classes/cacs/rest_poller_cac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from gwproto.data_classes.component_attribute_class import ComponentAttributeClass


class RESTPollerCac(ComponentAttributeClass):
...
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Optional

from gwproto.data_classes.component import Component


class FibaroSmartImplantComponent(Component):
def __init__(
self,
component_id: str,
component_attribute_class_id: str,
display_name: Optional[str] = None,
hw_uid: Optional[str] = None,
):
super().__init__(
component_id=component_id,
component_attribute_class_id=component_attribute_class_id,
display_name=display_name,
hw_uid=hw_uid,
)
29 changes: 29 additions & 0 deletions src/gwproto/data_classes/components/hubitat_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Optional

import yarl

from gwproto.data_classes.component import Component
from gwproto.types.hubitat_gt import HubitatGt


class HubitatComponent(Component):
hubitat_gt: HubitatGt

def __init__(
self,
component_id: str,
component_attribute_class_id: str,
hubitat_gt: HubitatGt,
display_name: Optional[str] = None,
hw_uid: Optional[str] = None,
):
self.hubitat_gt = hubitat_gt
super().__init__(
component_id=component_id,
component_attribute_class_id=component_attribute_class_id,
display_name=display_name,
hw_uid=hw_uid,
)

def urls(self) -> dict[str, Optional[yarl.URL]]:
return self.hubitat_gt.urls()
113 changes: 113 additions & 0 deletions src/gwproto/data_classes/components/hubitat_tank_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from typing import Optional

import yarl

from gwproto.data_classes.component import Component
from gwproto.data_classes.components.hubitat_component import HubitatComponent
from gwproto.data_classes.resolver import ComponentResolver
from gwproto.data_classes.sh_node import ShNode
from gwproto.types.hubitat_component_gt import HubitatComponentGt
from gwproto.types.hubitat_component_gt import HubitatRESTResolutionSettings
from gwproto.types.hubitat_gt import HubitatGt
from gwproto.types.hubitat_tank_gt import FibaroTempSensorSettings
from gwproto.types.hubitat_tank_gt import FibaroTempSensorSettingsGt
from gwproto.types.hubitat_tank_gt import HubitatTankSettingsGt
from gwproto.types.telemetry_reporting_config import TelemetryReportingConfig


class HubitatTankComponent(Component, ComponentResolver):
hubitat: HubitatComponentGt
sensor_supply_voltage: float
devices_gt: list[FibaroTempSensorSettingsGt]
devices: list[FibaroTempSensorSettings] = []

def __init__(
self,
component_id: str,
component_attribute_class_id: str,
tank_gt: HubitatTankSettingsGt,
display_name: Optional[str] = None,
hw_uid: Optional[str] = None,
):
# Create self.hubitat as a proxy containing only the id
# of the hubitat; the actual component data will be resolved
# when resolve() is called; Here in the constructor we cannot
# rely on the actual HubitatComponentGt existing yet.
self.hubitat = HubitatComponentGt(
ComponentId=tank_gt.hubitat_component_id,
ComponentAttributeClassId="00000000-0000-0000-0000-000000000000",
Hubitat=HubitatGt(
Host="",
MakerApiId=-1,
AccessToken="",
MacAddress="000000000000",
),
)
self.sensor_supply_voltage = tank_gt.sensor_supply_voltage
self.devices_gt = list(tank_gt.devices)
super().__init__(
display_name=display_name,
component_id=component_id,
hw_uid=hw_uid,
component_attribute_class_id=component_attribute_class_id,
)

def resolve(
self,
tank_node_name: str,
nodes: dict[str, ShNode],
components: dict[str, Component],
):
hubitat_component = components.get(self.hubitat.ComponentId, None)
if hubitat_component is None:
raise ValueError(
"ERROR. No component found for "
f"HubitatTankComponent.hubitat.CompnentId {self.hubitat.ComponentId}"
)
if not isinstance(hubitat_component, HubitatComponent):
raise ValueError(
"ERROR. Referenced hubitat component has type "
f"{type(hubitat_component)}; "
"must be instance of HubitatComponent. "
f"Hubitat component id: {self.hubitat.ComponentId}"
)
hubitat_component_gt = HubitatComponentGt.from_data_class(hubitat_component)
hubitat_settings = HubitatRESTResolutionSettings(hubitat_component_gt)
devices = [
FibaroTempSensorSettings.create(
tank_name=tank_node_name,
settings_gt=device_gt,
hubitat=hubitat_settings,
)
for device_gt in self.devices_gt
if device_gt.enabled
]
for device in devices:
if device.node_name not in nodes:
raise ValueError(
f"ERROR. Node not found for tank temp sensor <{device.node_name}>"
)
# replace proxy hubitat component, which only had component id.
# with the actual hubitat component containing data.
self.hubitat = hubitat_component_gt
self.devices = devices

def urls(self) -> dict[str, Optional[yarl.URL]]:
urls = self.hubitat.urls()
for device in self.devices:
urls[device.node_name] = device.url
return urls

@property
def config_list(self) -> list[TelemetryReportingConfig]:
return [
TelemetryReportingConfig(
TelemetryName=device.telemetry_name,
AboutNodeName=device.node_name,
ReportOnChange=False,
SamplePeriodS=int(device.rest.poll_period_seconds),
Exponent=device.exponent,
Unit=device.unit,
)
for device in self.devices
]
24 changes: 24 additions & 0 deletions src/gwproto/data_classes/components/rest_poller_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.rest_poller_gt import RESTPollerSettings


class RESTPollerComponent(Component):
rest: RESTPollerSettings

def __init__(
self,
component_id: str,
component_attribute_class_id: str,
rest: RESTPollerSettings,
display_name: Optional[str] = None,
hw_uid: Optional[str] = None,
):
self.rest = rest
super().__init__(
display_name=display_name,
component_id=component_id,
hw_uid=hw_uid,
component_attribute_class_id=component_attribute_class_id,
)
Loading

0 comments on commit fc1aefc

Please sign in to comment.