Skip to content
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
26 changes: 25 additions & 1 deletion custom_components/eyeonwater/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Description:
key: str
device_class: BinarySensorDeviceClass
translation_key: str | None = None
enabled_by_default: bool = True


FLAG_SENSORS = [
Expand All @@ -37,6 +38,12 @@ class Description:
translation_key="leak",
device_class=BinarySensorDeviceClass.MOISTURE,
),
Description(
key="encoder_leak",
translation_key="encoderleak",
device_class=BinarySensorDeviceClass.MOISTURE,
enabled_by_default=False,
),
Description(
key="empty_pipe",
translation_key="emptypipe",
Expand Down Expand Up @@ -65,6 +72,18 @@ class Description:
key="battery_charging",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
),
Description(
key="endpoint_reading_missed",
translation_key="endpointreadingmissed",
device_class=BinarySensorDeviceClass.PROBLEM,
enabled_by_default=False,
),
Description(
key="device_alert",
translation_key="devicealert",
device_class=BinarySensorDeviceClass.PROBLEM,
enabled_by_default=False,
),
]


Expand Down Expand Up @@ -104,6 +123,7 @@ def __init__(
key=description.key,
device_class=description.device_class,
translation_key=description.translation_key,
entity_registry_enabled_default=description.enabled_by_default,
)
self.meter = meter
self._uuid = normalize_id(meter.meter_uuid)
Expand All @@ -124,7 +144,11 @@ def __init__(
def get_flag(self) -> bool:
"""Get flag value."""
return bool(
self.meter.meter_info.reading.flags.__dict__[self.entity_description.key],
getattr(
self.meter.meter_info.reading.flags,
self.entity_description.key,
False,
),
)

@callback
Expand Down
2 changes: 1 addition & 1 deletion custom_components/eyeonwater/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/kdeyev/eyeonwater/issues",
"requirements": ["pyonwater==0.3.32"],
"version": "2.7.9-beta.1"
"version": "2.7.10-beta.1"
}
9 changes: 9 additions & 0 deletions custom_components/eyeonwater/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"leak": {
"name": "Leak Sensor"
},
"encoderleak": {
"name": "Encoder Leak"
},
"emptypipe": {
"name": "Empty Pipe"
},
Expand All @@ -50,6 +53,12 @@
},
"reverseflow": {
"name": "Reverse Waterflow"
},
"endpointreadingmissed": {
"name": "Endpoint Reading Missed"
},
"devicealert": {
"name": "Device Alert"
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions custom_components/eyeonwater/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"leak": {
"name": "Leak Sensor"
},
"encoderleak": {
"name": "Encoder Leak"
},
"emptypipe": {
"name": "Empty Pipe"
},
Expand All @@ -52,6 +55,12 @@
},
"reverseflow": {
"name": "Reverse Waterflow"
},
"endpointreadingmissed": {
"name": "Endpoint Reading Missed"
},
"devicealert": {
"name": "Device Alert"
}
}
},
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ class FakeFlags:
"""Fake meter flag data for testing."""

leak: bool = False
encoder_leak: bool = False
empty_pipe: bool = False
tamper: bool = False
cover_removed: bool = False
reverse_flow: bool = False
low_battery: bool = False
battery_charging: bool = False
endpoint_reading_missed: bool = False
device_alert: bool = False


@dataclass
Expand Down
153 changes: 153 additions & 0 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Tests for EyeOnWater binary sensor entities."""

from unittest.mock import MagicMock

import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from custom_components.eyeonwater.binary_sensor import (
FLAG_SENSORS,
Description,
EyeOnWaterBinarySensor,
)
from custom_components.eyeonwater.statistic_helper import normalize_id

from .conftest import MOCK_METER_UUID, _make_meter


@pytest.fixture
def coordinator() -> MagicMock:
"""Provide a mock coordinator."""
coord = MagicMock(spec=DataUpdateCoordinator)
coord.last_update_success = True
coord.async_add_listener = MagicMock(return_value=lambda: None)
return coord


def _make_sensor(key: str, coordinator: MagicMock) -> EyeOnWaterBinarySensor:
"""Return a sensor for the given flag key."""
desc = next(d for d in FLAG_SENSORS if d.key == key)
return EyeOnWaterBinarySensor(_make_meter(), coordinator, desc)


# ---------- FLAG_SENSORS catalog ----------


class TestFlagSensorsCatalog:
"""Verify the FLAG_SENSORS list contains expected entries."""

def test_encoder_leak_present(self) -> None:
"""encoder_leak sensor must exist in FLAG_SENSORS."""
keys = [d.key for d in FLAG_SENSORS]
assert "encoder_leak" in keys

def test_endpoint_reading_missed_present(self) -> None:
"""endpoint_reading_missed sensor must exist in FLAG_SENSORS."""
keys = [d.key for d in FLAG_SENSORS]
assert "endpoint_reading_missed" in keys

def test_device_alert_present(self) -> None:
"""device_alert sensor must exist in FLAG_SENSORS."""
keys = [d.key for d in FLAG_SENSORS]
assert "device_alert" in keys

def test_encoder_leak_disabled_by_default(self) -> None:
"""encoder_leak must be disabled by default."""
desc = next(d for d in FLAG_SENSORS if d.key == "encoder_leak")
assert desc.enabled_by_default is False

def test_endpoint_reading_missed_disabled_by_default(self) -> None:
"""endpoint_reading_missed must be disabled by default."""
desc = next(d for d in FLAG_SENSORS if d.key == "endpoint_reading_missed")
assert desc.enabled_by_default is False

def test_device_alert_disabled_by_default(self) -> None:
"""device_alert must be disabled by default."""
desc = next(d for d in FLAG_SENSORS if d.key == "device_alert")
assert desc.enabled_by_default is False

def test_leak_enabled_by_default(self) -> None:
"""Primary leak sensor must remain enabled by default."""
desc = next(d for d in FLAG_SENSORS if d.key == "leak")
assert desc.enabled_by_default is True

def test_encoder_leak_is_moisture(self) -> None:
"""encoder_leak must have MOISTURE device class."""
desc = next(d for d in FLAG_SENSORS if d.key == "encoder_leak")
assert desc.device_class == BinarySensorDeviceClass.MOISTURE

def test_endpoint_reading_missed_is_problem(self) -> None:
"""endpoint_reading_missed must have PROBLEM device class."""
desc = next(d for d in FLAG_SENSORS if d.key == "endpoint_reading_missed")
assert desc.device_class == BinarySensorDeviceClass.PROBLEM

def test_device_alert_is_problem(self) -> None:
"""device_alert must have PROBLEM device class."""
desc = next(d for d in FLAG_SENSORS if d.key == "device_alert")
assert desc.device_class == BinarySensorDeviceClass.PROBLEM


# ---------- EyeOnWaterBinarySensor ----------


class TestEyeOnWaterBinarySensor:
"""Tests for EyeOnWaterBinarySensor entity behaviour."""

def test_unique_id_encoder_leak(self, coordinator: MagicMock) -> None:
"""Unique ID must include sensor key and normalized UUID."""
sensor = _make_sensor("encoder_leak", coordinator)
assert sensor._attr_unique_id == f"encoder_leak_{normalize_id(MOCK_METER_UUID)}"

def test_entity_registry_disabled_encoder_leak(
self,
coordinator: MagicMock,
) -> None:
"""encoder_leak entity must be disabled in the entity registry by default."""
sensor = _make_sensor("encoder_leak", coordinator)
assert sensor.entity_description.entity_registry_enabled_default is False

def test_entity_registry_disabled_endpoint_reading_missed(
self,
coordinator: MagicMock,
) -> None:
"""endpoint_reading_missed entity must be disabled in the entity registry by default."""
sensor = _make_sensor("endpoint_reading_missed", coordinator)
assert sensor.entity_description.entity_registry_enabled_default is False

def test_entity_registry_disabled_device_alert(
self,
coordinator: MagicMock,
) -> None:
"""device_alert entity must be disabled in the entity registry by default."""
sensor = _make_sensor("device_alert", coordinator)
assert sensor.entity_description.entity_registry_enabled_default is False

def test_entity_registry_enabled_leak(self, coordinator: MagicMock) -> None:
"""Primary leak sensor must be enabled in the entity registry by default."""
sensor = _make_sensor("leak", coordinator)
assert sensor.entity_description.entity_registry_enabled_default is True

def test_get_flag_true(self, coordinator: MagicMock) -> None:
"""get_flag returns True when the flag field is set on the meter."""
meter = _make_meter()
meter.meter_info.reading.flags.encoder_leak = True
desc = next(d for d in FLAG_SENSORS if d.key == "encoder_leak")
sensor = EyeOnWaterBinarySensor(meter, coordinator, desc)
assert sensor.get_flag() is True

def test_get_flag_false(self, coordinator: MagicMock) -> None:
"""get_flag returns False when the flag field is unset."""
sensor = _make_sensor("encoder_leak", coordinator)
assert sensor.get_flag() is False

def test_get_flag_missing_field_returns_false(self, coordinator: MagicMock) -> None:
"""get_flag returns False gracefully for flags not present on the meter."""
meter = _make_meter()
# FakeFlags does not define 'nonexistent_flag', so getattr returns the default False
desc = Description(
key="nonexistent_flag",
device_class=BinarySensorDeviceClass.PROBLEM,
)
sensor = EyeOnWaterBinarySensor(meter, coordinator, desc)
assert sensor.get_flag() is False
Loading