Skip to content

Commit a510303

Browse files
author
DAB
committed
refactor: move next_watering to device-level BHyveSensor
Per review feedback: - Move next_watering from per-zone (BHyveZoneSensor) to device-level (SENSOR_TYPES_SPRINKLER), using the existing BHyveSensor class - Remove SENSOR_TYPES_ZONE tuple and BHyveZoneSensor class - Make programs attribute conditional: only set when next_start_programs is present in device status - Enable by default (drop entity_registry_enabled_default=False) - Fix missing device_class passthrough in sprinkler sensor description copy (pre-existing bug, exposed by TIMESTAMP rendering requirement) - Update tests and README to reflect device-level scope
1 parent f34a473 commit a510303

3 files changed

Lines changed: 65 additions & 100 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ _Note_: The Wifi hub is required to provide the flood sensors with internet conn
3232
## Supported Entities
3333

3434
- `valve` for opening/closing individual zones on `sprinkler_timer` devices.
35-
- `sensor` for battery levels, zone watering history, zone next watering time, device run-mode state, flood-sensor temperature and flood-sensor signal strength.
35+
- `sensor` for battery levels, zone watering history, device next watering time, device run-mode state, flood-sensor temperature and flood-sensor signal strength.
3636
- `switch` for enabling/disabling rain delays, toggling pre-configured programs and enabling/disabling per-zone smart watering.
3737
- `select` for the device run-mode (auto/off) on `sprinkler_timer` devices.
3838
- `binary_sensor` for flood detection, temperature alerts, sprinkler station faults and Wi-Fi bridge connectivity.
@@ -94,17 +94,17 @@ The following attributes are set on zone history sensor entities:
9494
| `consumption_litres` | `number` | The amount of water consumed, in litres. |
9595
| `start_time` | `string` | The start time of the watering. |
9696

97-
### Zone Next Watering sensor
97+
### Device Next Watering sensor
9898

99-
A **next watering** `sensor` entity is created for each zone on a `sprinkler_timer` device. This reports the next scheduled watering time as a timestamp, allowing relative-time rendering ("in 14 hours"), long-term statistics, and direct use in automations and dashboard cards.
99+
A **next watering** `sensor` entity is created for each `sprinkler_timer` device. This reports the next scheduled watering time as a timestamp, allowing relative-time rendering ("in 14 hours"), long-term statistics, and direct use in automations and dashboard cards.
100100

101101
The existing `next_start_time` valve attribute is preserved for backward compatibility.
102102

103103
The following attributes are set on next watering sensor entities:
104104

105-
| Attribute | Type | Notes |
106-
| ---------- | -------------- | ----------------------------------------------- |
107-
| `programs` | `list[string]` | The programs scheduled to run at this time. |
105+
| Attribute | Type | Notes |
106+
| ---------- | -------------- | -------------------------------------------------------------- |
107+
| `programs` | `list[string]` | The programs scheduled to run at this time, if any. |
108108

109109
### Temperature sensor
110110

custom_components/bhyve/sensor.py

Lines changed: 8 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -104,22 +104,21 @@ class BHyveSensorEntityDescription(SensorEntityDescription):
104104
entity_category=EntityCategory.DIAGNOSTIC,
105105
value_fn=lambda data: data.get("status", {}).get("run_mode", "unavailable"),
106106
),
107-
)
108-
109-
SENSOR_TYPES_ZONE: tuple[BHyveSensorEntityDescription, ...] = (
110107
BHyveSensorEntityDescription(
111108
key="next_watering",
112109
translation_key="next_watering",
110+
name="Next watering",
113111
unique_id_suffix="next_watering",
114112
device_class=SensorDeviceClass.TIMESTAMP,
115113
icon="mdi:sprinkler-variant",
116-
entity_registry_enabled_default=False,
117114
value_fn=lambda data: orbit_time_to_local_time(
118115
data.get("status", {}).get("next_start_time")
119116
),
120-
attributes_fn=lambda data: {
121-
ATTR_NEXT_START_PROGRAMS: data.get("status", {}).get("next_start_programs"),
122-
},
117+
attributes_fn=lambda data: (
118+
{ATTR_NEXT_START_PROGRAMS: programs}
119+
if (programs := data.get("status", {}).get("next_start_programs"))
120+
else {}
121+
),
123122
),
124123
)
125124

@@ -206,6 +205,7 @@ async def async_setup_entry(
206205
name=base_description.name,
207206
icon=base_description.icon,
208207
unique_id_suffix=base_description.unique_id_suffix,
208+
device_class=base_description.device_class,
209209
entity_category=base_description.entity_category,
210210
value_fn=base_description.value_fn,
211211
attributes_fn=base_description.attributes_fn,
@@ -240,18 +240,6 @@ async def async_setup_entry(
240240
)
241241
)
242242

243-
# Add per-zone sensors (next watering, etc.)
244-
for base_description in SENSOR_TYPES_ZONE:
245-
sensors.append( # noqa: PERF401
246-
BHyveZoneSensor(
247-
coordinator,
248-
device,
249-
zone,
250-
zone_name,
251-
base_description,
252-
)
253-
)
254-
255243
# Add battery sensor if device has battery
256244
if device.get("battery", None) is not None:
257245
for base_description in SENSOR_TYPES_BATTERY:
@@ -335,7 +323,7 @@ def __init__(
335323
)
336324

337325
@property
338-
def native_value(self) -> int | float | str | None:
326+
def native_value(self) -> datetime | int | float | str | None:
339327
"""Return the state of the entity."""
340328
if self.entity_description.value_fn:
341329
return self.entity_description.value_fn(self.device_data)
@@ -457,45 +445,3 @@ def _get_device_history(self) -> list:
457445
)
458446

459447

460-
class BHyveZoneSensor(BHyveCoordinatorEntity, SensorEntity):
461-
"""Define a BHyve per-zone sensor."""
462-
463-
entity_description: BHyveSensorEntityDescription
464-
_attr_has_entity_name = True
465-
_attr_entity_registry_enabled_default = False
466-
467-
def __init__(
468-
self,
469-
coordinator: BHyveDataUpdateCoordinator,
470-
device: BHyveDevice,
471-
zone: dict,
472-
zone_name: str,
473-
description: BHyveSensorEntityDescription,
474-
) -> None:
475-
"""Initialize the sensor."""
476-
self.entity_description = description
477-
friendly = description.key.replace("_", " ").title()
478-
if zone_name == device.get("name"):
479-
self._attr_name = friendly
480-
else:
481-
self._attr_name = f"{zone_name} {friendly}"
482-
super().__init__(coordinator, device)
483-
self._zone = zone
484-
self._zone_id = zone.get("station")
485-
self._attr_unique_id = (
486-
f"{self._mac_address}:{self._device_id}:{self._zone_id}:{description.unique_id_suffix}"
487-
)
488-
489-
@property
490-
def native_value(self) -> datetime | None:
491-
"""Return the state of the entity."""
492-
if self.entity_description.value_fn:
493-
return self.entity_description.value_fn(self.device_data)
494-
return None
495-
496-
@property
497-
def extra_state_attributes(self) -> dict[str, Any]:
498-
"""Return the device state attributes."""
499-
if self.entity_description.attributes_fn:
500-
return self.entity_description.attributes_fn(self.device_data)
501-
return {}

tests/test_sensor.py

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@
1616
SENSOR_TYPES_BATTERY,
1717
SENSOR_TYPES_FLOOD,
1818
SENSOR_TYPES_SPRINKLER,
19-
SENSOR_TYPES_ZONE,
2019
BHyveSensor,
2120
BHyveSensorEntityDescription,
2221
BHyveZoneHistorySensor,
23-
BHyveZoneSensor,
2422
)
2523

2624
# Test constants
@@ -138,7 +136,7 @@ def mock_zone_history_data() -> list:
138136

139137
@pytest.fixture
140138
def mock_sprinkler_device_with_next_start_time() -> BHyveDevice:
141-
"""Mock BHyve sprinkler device with next_start_time in status."""
139+
"""Mock BHyve sprinkler device with next_start_time in device status."""
142140
return BHyveDevice(
143141
{
144142
"id": "test-device-123",
@@ -159,6 +157,27 @@ def mock_sprinkler_device_with_next_start_time() -> BHyveDevice:
159157
)
160158

161159

160+
@pytest.fixture
161+
def mock_sprinkler_device_no_schedule() -> BHyveDevice:
162+
"""Mock BHyve sprinkler device with no upcoming schedule."""
163+
return BHyveDevice(
164+
{
165+
"id": "test-device-123",
166+
"name": "Test Sprinkler",
167+
"type": "sprinkler_timer",
168+
"mac_address": "aa:bb:cc:dd:ee:ff",
169+
"hardware_version": "v2.0",
170+
"firmware_version": "1.2.3",
171+
"is_connected": True,
172+
"status": {
173+
"run_mode": "auto",
174+
"watering_status": None,
175+
},
176+
"zones": [{"station": "1", "name": "Front Lawn"}],
177+
}
178+
)
179+
180+
162181
class TestBHyveBatterySensor:
163182
"""Test BHyveBatterySensor entity."""
164183

@@ -627,43 +646,42 @@ async def test_sensors_handle_device_connection_events(
627646
assert state_sensor.available is False
628647

629648

630-
class TestBHyveZoneSensor:
631-
"""Test BHyveZoneSensor entity (next watering)."""
649+
class TestBHyveNextWateringSensor:
650+
"""Test next watering device-level sensor (SENSOR_TYPES_SPRINKLER[1])."""
632651

633652
async def test_next_watering_sensor_initialization(
634653
self,
635-
mock_sprinkler_device_with_battery: BHyveDevice,
654+
mock_sprinkler_device_with_next_start_time: BHyveDevice,
636655
) -> None:
637656
"""Test next watering sensor entity initialization."""
638657
coordinator = create_mock_coordinator(
639658
{
640659
"test-device-123": {
641-
"device": mock_sprinkler_device_with_battery,
660+
"device": mock_sprinkler_device_with_next_start_time,
642661
"history": [],
643662
"landscapes": {},
644663
}
645664
}
646665
)
647666

648-
zone = {"station": "1", "name": "Front Lawn"}
649-
sensor = BHyveZoneSensor(
667+
description = create_sensor_description(
668+
mock_sprinkler_device_with_next_start_time, SENSOR_TYPES_SPRINKLER[1]
669+
)
670+
sensor = BHyveSensor(
650671
coordinator=coordinator,
651-
device=mock_sprinkler_device_with_battery,
652-
zone=zone,
653-
zone_name="Front Lawn",
654-
description=SENSOR_TYPES_ZONE[0],
672+
device=mock_sprinkler_device_with_next_start_time,
673+
description=description,
655674
)
656675

657-
assert sensor._attr_name == "Front Lawn Next Watering"
676+
assert sensor.name == "Next watering"
658677
assert sensor.device_class == SensorDeviceClass.TIMESTAMP
659-
assert sensor._zone_id == "1"
660-
assert sensor._attr_unique_id.endswith(":1:next_watering")
678+
assert sensor._attr_unique_id.endswith(":next_watering")
661679

662680
async def test_next_watering_sensor_with_scheduled_time(
663681
self,
664682
mock_sprinkler_device_with_next_start_time: BHyveDevice,
665683
) -> None:
666-
"""Test next watering sensor returns correct timestamp and programs attribute."""
684+
"""Test next watering sensor returns correct timestamp and programs."""
667685
coordinator = create_mock_coordinator(
668686
{
669687
"test-device-123": {
@@ -674,13 +692,13 @@ async def test_next_watering_sensor_with_scheduled_time(
674692
}
675693
)
676694

677-
zone = {"station": "1", "name": "Front Lawn"}
678-
sensor = BHyveZoneSensor(
695+
description = create_sensor_description(
696+
mock_sprinkler_device_with_next_start_time, SENSOR_TYPES_SPRINKLER[1]
697+
)
698+
sensor = BHyveSensor(
679699
coordinator=coordinator,
680700
device=mock_sprinkler_device_with_next_start_time,
681-
zone=zone,
682-
zone_name="Front Lawn",
683-
description=SENSOR_TYPES_ZONE[0],
701+
description=description,
684702
)
685703

686704
# Value should parse to a datetime
@@ -689,34 +707,35 @@ async def test_next_watering_sensor_with_scheduled_time(
689707
assert sensor.native_value.month == 5
690708
assert sensor.native_value.day == 1
691709

692-
# Programs attribute should reflect next_start_programs
710+
# Programs attribute should be present when next_start_programs exists
693711
attrs = sensor.extra_state_attributes
694712
assert attrs["programs"] == ["e"]
695713

696714
async def test_next_watering_sensor_no_schedule(
697715
self,
698-
mock_sprinkler_device_with_battery: BHyveDevice,
716+
mock_sprinkler_device_no_schedule: BHyveDevice,
699717
) -> None:
700718
"""Test next watering sensor returns None when next_start_time is absent."""
701719
coordinator = create_mock_coordinator(
702720
{
703721
"test-device-123": {
704-
"device": mock_sprinkler_device_with_battery,
722+
"device": mock_sprinkler_device_no_schedule,
705723
"history": [],
706724
"landscapes": {},
707725
}
708726
}
709727
)
710728

711-
zone = {"station": "1", "name": "Front Lawn"}
712-
sensor = BHyveZoneSensor(
729+
description = create_sensor_description(
730+
mock_sprinkler_device_no_schedule, SENSOR_TYPES_SPRINKLER[1]
731+
)
732+
sensor = BHyveSensor(
713733
coordinator=coordinator,
714-
device=mock_sprinkler_device_with_battery,
715-
zone=zone,
716-
zone_name="Front Lawn",
717-
description=SENSOR_TYPES_ZONE[0],
734+
device=mock_sprinkler_device_no_schedule,
735+
description=description,
718736
)
719737

720738
# No next_start_time in status — should return None (HA renders as Unknown)
721739
assert sensor.native_value is None
722-
assert sensor.extra_state_attributes["programs"] is None
740+
# No programs attribute when there is no schedule
741+
assert sensor.extra_state_attributes == {}

0 commit comments

Comments
 (0)