Skip to content

Commit

Permalink
Use a more common-sense approach instead of trusting the documentation
Browse files Browse the repository at this point in the history
* Add several new entities that aren't documented:
* * Per phase energy delivered/received for most PowerTags
* * Add total variant for reactive energy received for PowerTag M-series
* Make PowerTag temperature entity be firmware dependent (only 004.xxx.xxx and up seem to have this)
* Remove legacy entities that are replaced by these new ones. These become 'Unavailable' and can be removed manually.
* Fix temperature warning. This requires resetting state-history.
* Fix reset buttons for Panel Server to also reset per-phase energy.
  • Loading branch information
Breina committed May 25, 2024
1 parent 9b8dffd commit 0203a30
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 190 deletions.
12 changes: 6 additions & 6 deletions custom_components/powertag_gateway/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
FeatureClass.TEMP0, FeatureClass.TEMP1, FeatureClass.CO2]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


Expand All @@ -85,7 +85,7 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
FeatureClass.TEMP0, FeatureClass.TEMP1, FeatureClass.CO2]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


Expand Down Expand Up @@ -119,7 +119,7 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


Expand All @@ -139,7 +139,7 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.TEMP1, FeatureClass.CO2]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.PANEL_SERVER]


Expand Down Expand Up @@ -172,7 +172,7 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK]


Expand Down Expand Up @@ -201,5 +201,5 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.PANEL_SERVER]
76 changes: 39 additions & 37 deletions custom_components/powertag_gateway/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .entity_base import WirelessDeviceEntity, setup_entities
from .device_features import FeatureClass
from .entity_base import WirelessDeviceEntity, setup_entities
from .schneider_modbus import SchneiderModbus, TypeOfGateway

_LOGGER = logging.getLogger(__name__)
Expand All @@ -16,11 +16,11 @@
def list_buttons() -> list[type[WirelessDeviceEntity]]:
return [
PowerTagResetPeakDemand,
PowerTagResetActiveEnergy,
PowerTagResetActiveEnergyDelivered,
PowerTagResetActiveEnergyReceived,
PowerTagResetReactiveEnergyDelivered,
PowerTagResetReactiveEnergyReceived
PowerTagResetReactiveEnergyReceived,
PowerTagResetApparentEnergy
]


Expand Down Expand Up @@ -49,18 +49,16 @@ def reset(self):

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.A1, FeatureClass.A2, FeatureClass.P1, FeatureClass.F1, FeatureClass.F2,
FeatureClass.F3, FeatureClass.FL, FeatureClass.M0, FeatureClass.M1, FeatureClass.M2,
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]
return feature_class in [FeatureClass.C]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


class PowerTagResetActiveEnergy(WirelessDeviceEntity, ButtonEntity):
class PowerTagResetActiveEnergyDelivered(WirelessDeviceEntity, ButtonEntity):
def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: DeviceInfo):
super().__init__(client, modbus_index, tag_device, "reset active energy")
super().__init__(client, modbus_index, tag_device, "reset active energy delivered")

def press(self) -> None:
self.reset()
Expand All @@ -69,20 +67,22 @@ async def async_press(self) -> None:
self.reset()

def reset(self):
self._client.tag_reset_energy_active_partial(self._modbus_index)
self._client.tag_reset_energy_active_delivered_partial(self._modbus_index)

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.FL, FeatureClass.R1]
return feature_class in [FeatureClass.A1, FeatureClass.F1 , FeatureClass.F3, FeatureClass.FL,
FeatureClass.M0, FeatureClass.M1, FeatureClass.M2, FeatureClass.M3,
FeatureClass.R1]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


class PowerTagResetActiveEnergyDelivered(WirelessDeviceEntity, ButtonEntity):
class PowerTagResetActiveEnergyReceived(WirelessDeviceEntity, ButtonEntity):
def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: DeviceInfo):
super().__init__(client, modbus_index, tag_device, "reset active energy delivered")
super().__init__(client, modbus_index, tag_device, "reset active energy received")

def press(self) -> None:
self.reset()
Expand All @@ -91,20 +91,22 @@ async def async_press(self) -> None:
self.reset()

def reset(self):
self._client.tag_reset_energy_active_delivered_partial(self._modbus_index)
self._client.tag_reset_energy_active_received_partial(self._modbus_index)

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.FL, FeatureClass.R1]
return feature_class in [FeatureClass.A1, FeatureClass.F1, FeatureClass.F3, FeatureClass.FL,
FeatureClass.M0, FeatureClass.M1, FeatureClass.M2, FeatureClass.M3,
FeatureClass.R1]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


class PowerTagResetActiveEnergyReceived(WirelessDeviceEntity, ButtonEntity):
class PowerTagResetReactiveEnergyDelivered(WirelessDeviceEntity, ButtonEntity):
def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: DeviceInfo):
super().__init__(client, modbus_index, tag_device, "reset active energy received")
super().__init__(client, modbus_index, tag_device, "reset reactive energy delivered")

def press(self) -> None:
self.reset()
Expand All @@ -113,20 +115,22 @@ async def async_press(self) -> None:
self.reset()

def reset(self):
self._client.tag_reset_energy_active_received_partial(self._modbus_index)
self._client.tag_reset_energy_reactive_delivered_partial(self._modbus_index)

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.FL, FeatureClass.R1]
return feature_class in [
FeatureClass.FL, FeatureClass.M0, FeatureClass.M1, FeatureClass.M2, FeatureClass.M3, FeatureClass.R1
]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
return type_of_gateway in [TypeOfGateway.SMARTLINK, TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


class PowerTagResetReactiveEnergyDelivered(WirelessDeviceEntity, ButtonEntity):
class PowerTagResetReactiveEnergyReceived(WirelessDeviceEntity, ButtonEntity):
def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: DeviceInfo):
super().__init__(client, modbus_index, tag_device, "reset reactive energy delivered")
super().__init__(client, modbus_index, tag_device, "reset reactive energy received")

def press(self) -> None:
self.reset()
Expand All @@ -135,22 +139,22 @@ async def async_press(self) -> None:
self.reset()

def reset(self):
self._client.tag_reset_energy_reactive_delivered_partial(self._modbus_index)
self._client.tag_reset_energy_reactive_received_partial(self._modbus_index)

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.A1, FeatureClass.A2, FeatureClass.P1, FeatureClass.F1, FeatureClass.F2,
FeatureClass.F3, FeatureClass.FL, FeatureClass.M0, FeatureClass.M1, FeatureClass.M2,
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]
return feature_class in [
FeatureClass.FL, FeatureClass.M0, FeatureClass.M1, FeatureClass.M2, FeatureClass.M3, FeatureClass.R1
]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]


class PowerTagResetReactiveEnergyReceived(WirelessDeviceEntity, ButtonEntity):
class PowerTagResetApparentEnergy(WirelessDeviceEntity, ButtonEntity):
def __init__(self, client: SchneiderModbus, modbus_index: int, tag_device: DeviceInfo):
super().__init__(client, modbus_index, tag_device, "reset reactive energy received")
super().__init__(client, modbus_index, tag_device, "reset apparent energy")

def press(self) -> None:
self.reset()
Expand All @@ -159,14 +163,12 @@ async def async_press(self) -> None:
self.reset()

def reset(self):
self._client.tag_reset_energy_reactive_received_partial(self._modbus_index)
self._client.tag_reset_energy_apparent_partial(self._modbus_index)

@staticmethod
def supports_feature_set(feature_class: FeatureClass) -> bool:
return feature_class in [FeatureClass.A1, FeatureClass.A2, FeatureClass.P1, FeatureClass.F1, FeatureClass.F2,
FeatureClass.F3, FeatureClass.FL, FeatureClass.M0, FeatureClass.M1, FeatureClass.M2,
FeatureClass.M3, FeatureClass.R1, FeatureClass.C]
return feature_class in [FeatureClass.FL, FeatureClass.R1]

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
return type_of_gateway in [TypeOfGateway.POWERTAG_LINK, TypeOfGateway.PANEL_SERVER]
12 changes: 9 additions & 3 deletions custom_components/powertag_gateway/entity_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def __init__(self, client: SchneiderModbus, tag_device: DeviceInfo, sensor_name:
self._attr_unique_id = f"{TAG_DOMAIN}{serial}{sensor_name}"

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
raise NotImplementedError()


Expand All @@ -130,9 +130,13 @@ def supports_feature_set(feature_class: FeatureClass) -> bool:
raise NotImplementedError()

@staticmethod
def supports_gateway(type_of_gateway: TypeOfGateway):
def supports_gateway(type_of_gateway: TypeOfGateway) -> bool:
raise NotImplementedError()

@staticmethod
def supports_firmware_version(firmware_version: str) -> bool:
return True


def collect_entities(client: SchneiderModbus, entities: list[Entity], feature_class: FeatureClass, modbus_address: int,
powertag_entity: type[WirelessDeviceEntity], tag_device: DeviceInfo, tag_phase_sequence: PhaseSequence):
Expand Down Expand Up @@ -228,7 +232,9 @@ def setup_entities(hass: HomeAssistant, config_entry: ConfigEntry, powertag_enti

for powertag_entity in [
entity for entity in powertag_entities
if entity.supports_feature_set(feature_class) and entity.supports_gateway(client.type_of_gateway)
if entity.supports_feature_set(feature_class)
and entity.supports_gateway(client.type_of_gateway)
and entity.supports_firmware_version(tag_device['sw_version'])
]:
collect_entities(
client, entities, feature_class, modbus_address, powertag_entity, tag_device, tag_phase_sequence
Expand Down
2 changes: 1 addition & 1 deletion custom_components/powertag_gateway/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/Breina/PowerTagGateway/issues",
"requirements": ["pymodbus==3.5.4"],
"version": "0.4.2"
"version": "0.5.0"
}
48 changes: 36 additions & 12 deletions custom_components/powertag_gateway/schneider_modbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,35 +327,59 @@ def tag_energy_active_delivered_plus_received_total(self, tag_index: int) -> int
"""Total active energy delivered + received (not resettable)"""
return self.__read_int_64(0xC83, tag_index)

def tag_energy_active_delta(self, tag_index: int, phase: Phase) -> int | None:
"""Active energy on phase delivered - received (not resettable)"""
return self.__read_int_64(0xC8F + phase.value * 2, tag_index)

def tag_energy_active_delivered_plus_received_partial(self, tag_index: int) -> int | None:
"""Partial active energy delivered + received (resettable)"""
return self.__read_int_64(0xCB7, tag_index)

def tag_reset_energy_active_partial(self, tag_index: int):
"""Set partial active energy counter. The value returns to zero by PowerTag Link gateway"""
self.__write_int_64(0xCBB, tag_index, 1)
# Energy Data – New Zone

def tag_reset_energy_active_delivered_partial(self, tag_index: int):
"""Set partial active energy delivered counter. The value returns to zero by PowerTag Link gateway"""
self.__write_int_64(0xCC3, tag_index, 1)
if self.type_of_gateway == TypeOfGateway.PANEL_SERVER:
self.__write_int_64(0x1390, tag_index, 0) # All
self.__write_int_64(0x13B8, tag_index, 0) # Phase A
self.__write_int_64(0x13E0, tag_index, 0) # Phase B
self.__write_int_64(0x1408, tag_index, 0) # Phase C
else:
self.__write_int_64(0xCC3, tag_index, 0)

def tag_reset_energy_active_received_partial(self, tag_index: int):
"""Set partial active energy received counter. The value returns to zero by PowerTag Link gateway."""
self.__write_int_64(0xCCB, tag_index, 1)
if self.type_of_gateway == TypeOfGateway.PANEL_SERVER:
self.__write_int_64(0x1398, tag_index, 0) # All
self.__write_int_64(0x13C0, tag_index, 0) # Phase A
self.__write_int_64(0x13E8, tag_index, 0) # Phase B
self.__write_int_64(0x1410, tag_index, 0) # Phase C
else:
self.__write_int_64(0xCCB, tag_index, 0)

def tag_reset_energy_reactive_delivered_partial(self, tag_index: int):
"""Set partial reactive energy delivered counter. The value returns to zero by PowerTag Link gateway."""
self.__write_int_64(0xCD3, tag_index, 1)
if self.type_of_gateway == TypeOfGateway.PANEL_SERVER:
self.__write_int_64(0x1438, tag_index, 0) # All
self.__write_int_64(0x1470, tag_index, 0) # Phase A
self.__write_int_64(0x1498, tag_index, 0) # Phase B
self.__write_int_64(0x14C0, tag_index, 0) # Phase C
else:
self.__write_int_64(0xCD3, tag_index, 0)

def tag_reset_energy_reactive_received_partial(self, tag_index: int):
"""Set partial reactive energy received counter. The value returns to zero by PowerTag Link gateway."""
self.__write_int_64(0xCDB, tag_index, 1)
if self.type_of_gateway == TypeOfGateway.PANEL_SERVER:
self.__write_int_64(0x1448, tag_index, 0) # All
self.__write_int_64(0x1478, tag_index, 0) # Phase A
self.__write_int_64(0x14A0, tag_index, 0) # Phase B
self.__write_int_64(0x14C8, tag_index, 0) # Phase C
else:
self.__write_int_64(0xCDB, tag_index, 0)

# Energy Data – New Zone
def tag_reset_energy_apparent_partial(self, tag_index: int):
"""Set partial apparent energy counter. The value returns to zero by PowerTag Link gateway."""
assert self.type_of_gateway == TypeOfGateway.PANEL_SERVER
self.__write_int_64(0x14F4, tag_index, 0) # All
self.__write_int_64(0x150C, tag_index, 0) # Phase A
self.__write_int_64(0x1534, tag_index, 0) # Phase B
self.__write_int_64(0x155C, tag_index, 0) # Phase C

def tag_energy_active_delivered_partial(self, tag_index: int) -> int | None:
"""Active energy delivered (resettable)"""
Expand Down
Loading

0 comments on commit 0203a30

Please sign in to comment.