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

Support Timestamp class Sensor #222

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
import math
from typing import Any, Optional
from unittest.mock import AsyncMock, MagicMock
Expand Down Expand Up @@ -394,6 +395,18 @@ async def async_test_pi_heating_demand(
assert_state(entity, 1, "%")


async def async_test_change_source_timestamp(
zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
"""Test change source timestamp is correctly returned."""
await send_attributes_report(
zha_gateway,
cluster,
{hvac.Thermostat.AttributeDefs.setpoint_change_source_timestamp.id: 2674725315},
)
assert entity.state["state"] == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC)


@pytest.mark.parametrize(
"cluster_id, entity_type, test_func, read_plug, unsupported_attrs",
(
Expand Down Expand Up @@ -547,6 +560,13 @@ async def async_test_pi_heating_demand(
None,
None,
),
(
hvac.Thermostat.cluster_id,
sensor.SetpointChangeSourceTimestamp,
async_test_change_source_timestamp,
None,
None,
),
),
)
async def test_sensor(
Expand Down
24 changes: 24 additions & 0 deletions tests/zha_devices_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5917,6 +5917,14 @@
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_setpoint_change_source",
},
(
"sensor",
"00:11:22:33:44:55:66:77-1-513-setpoint_change_source_timestamp",
): {
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSourceTimestamp",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_setpoint_change_source_timestamp",
},
("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): {
DEV_SIG_CLUSTER_HANDLERS: ["ota"],
DEV_SIG_ENT_MAP_CLASS: "FirmwareUpdateEntity",
Expand Down Expand Up @@ -6036,6 +6044,14 @@
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_setpoint_change_source",
},
(
"sensor",
"00:11:22:33:44:55:66:77-1-513-setpoint_change_source_timestamp",
): {
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSourceTimestamp",
DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_setpoint_change_source_timestamp",
},
("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): {
DEV_SIG_CLUSTER_HANDLERS: ["ota"],
DEV_SIG_ENT_MAP_CLASS: "FirmwareUpdateEntity",
Expand Down Expand Up @@ -6453,6 +6469,14 @@
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource",
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_setpoint_change_source",
},
(
"sensor",
"00:11:22:33:44:55:66:77-1-513-setpoint_change_source_timestamp",
): {
DEV_SIG_CLUSTER_HANDLERS: ["thermostat"],
DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSourceTimestamp",
DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_setpoint_change_source_timestamp",
},
("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): {
DEV_SIG_CLUSTER_HANDLERS: ["ota"],
DEV_SIG_ENT_MAP_CLASS: "FirmwareUpdateEntity",
Expand Down
39 changes: 36 additions & 3 deletions zha/application/platforms/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from asyncio import Task
from dataclasses import dataclass
from datetime import UTC, date, datetime
import enum
import functools
import logging
Expand All @@ -29,7 +30,12 @@
)
from zha.application.platforms.climate.const import HVACAction
from zha.application.platforms.helpers import validate_device_class
from zha.application.platforms.sensor.const import SensorDeviceClass, SensorStateClass
from zha.application.platforms.sensor.const import (
NON_NUMERIC_FORMATTERS,
UNIX_EPOCH_TO_ZCL_EPOCH,
SensorDeviceClass,
SensorStateClass,
)
from zha.application.registries import PLATFORM_ENTITIES
from zha.decorators import periodic
from zha.units import (
Expand Down Expand Up @@ -240,7 +246,7 @@ def state(self) -> dict:
return response

@property
def native_value(self) -> str | int | float | None:
def native_value(self) -> date | datetime | str | int | float | None:
"""Return the state of the entity."""
assert self._attribute_name is not None
raw_state = self._cluster_handler.cluster.get(self._attribute_name)
Expand All @@ -264,14 +270,27 @@ def handle_cluster_handler_attribute_updated(
):
self.maybe_emit_state_changed_event()

def formatter(self, value: int | enum.IntEnum) -> int | float | str | None:
def formatter(
self, value: int | enum.IntEnum
) -> date | datetime | int | float | str | None:
"""Pass-through formatter."""
return getattr(
self,
NON_NUMERIC_FORMATTERS.get(self._attr_device_class, "numeric_formatter"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be clearest to have a single formatter function for the base sensor (converter?) and then have each sub-type override it, having NumericSensor, StringSensor, and DateTimeSensor provide an implementation.

)(value)

def numeric_formatter(self, value: int | enum.IntEnum) -> int | float | str | None:
"""Numeric pass-through formatter."""
if self._decimals > 0:
return round(
float(value * self._multiplier) / self._divisor, self._decimals
)
return round(float(value * self._multiplier) / self._divisor)

def timestamp_formatter(self, value: int) -> datetime | None:
"""Timestamp pass-through formatter."""
return datetime.fromtimestamp(value - UNIX_EPOCH_TO_ZCL_EPOCH, tz=UTC)


class PollableSensor(Sensor):
"""Base ZHA sensor that polls for state."""
Expand Down Expand Up @@ -1631,6 +1650,20 @@ class SetpointChangeSource(EnumSensor):
_enum = SetpointChangeSourceEnum


@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
class SetpointChangeSourceTimestamp(Sensor):
"""Sensor that displays the timestamp the setpoint change.

Optional thermostat attribute.
"""

_unique_id_suffix = "setpoint_change_source_timestamp"
_attribute_name = "setpoint_change_source_timestamp"
_attr_translation_key: str = "setpoint_change_source_timestamp"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_device_class = SensorDeviceClass.TIMESTAMP


@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER)
class WindowCoveringTypeSensor(EnumSensor):
"""Sensor that displays the type of a cover device."""
Expand Down
6 changes: 6 additions & 0 deletions zha/application/platforms/sensor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,9 @@ class SensorDeviceClass(enum.StrEnum):
SensorDeviceClass.ENUM,
SensorDeviceClass.TIMESTAMP,
}

NON_NUMERIC_FORMATTERS = {
SensorDeviceClass.TIMESTAMP: "timestamp_formatter",
}

UNIX_EPOCH_TO_ZCL_EPOCH = 946684800
Loading