Add support for configuring built-in Melnor Bluetooth scheduling system (#93333)
This commit is contained in:
parent
e6a214595b
commit
accee4b5ef
10 changed files with 335 additions and 25 deletions
|
@ -12,5 +12,5 @@
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/melnor",
|
"documentation": "https://www.home-assistant.io/integrations/melnor",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["melnor-bluetooth==0.0.20"]
|
"requirements": ["melnor-bluetooth==0.0.21"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,13 @@ from typing import Any
|
||||||
|
|
||||||
from melnor_bluetooth.device import Valve
|
from melnor_bluetooth.device import Valve
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import (
|
||||||
|
NumberEntity,
|
||||||
|
NumberEntityDescription,
|
||||||
|
NumberMode,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory, UnitOfTime
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
@ -44,10 +48,33 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
|
||||||
native_min_value=1,
|
native_min_value=1,
|
||||||
icon="mdi:timer-cog-outline",
|
icon="mdi:timer-cog-outline",
|
||||||
key="manual_minutes",
|
key="manual_minutes",
|
||||||
name="Manual Minutes",
|
name="Manual Duration",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value),
|
set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value),
|
||||||
state_fn=lambda valve: valve.manual_watering_minutes,
|
state_fn=lambda valve: valve.manual_watering_minutes,
|
||||||
)
|
),
|
||||||
|
MelnorZoneNumberEntityDescription(
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_max_value=168,
|
||||||
|
native_min_value=1,
|
||||||
|
icon="mdi:calendar-refresh-outline",
|
||||||
|
key="frequency_interval_hours",
|
||||||
|
name="Schedule Interval",
|
||||||
|
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||||
|
set_num_fn=lambda valve, value: valve.set_frequency_interval_hours(value),
|
||||||
|
state_fn=lambda valve: valve.frequency.interval_hours,
|
||||||
|
),
|
||||||
|
MelnorZoneNumberEntityDescription(
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_max_value=360,
|
||||||
|
native_min_value=1,
|
||||||
|
icon="mdi:timer-outline",
|
||||||
|
key="frequency_duration_minutes",
|
||||||
|
name="Schedule Duration",
|
||||||
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
|
set_num_fn=lambda valve, value: valve.set_frequency_duration_minutes(value),
|
||||||
|
state_fn=lambda valve: valve.frequency.duration_minutes,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,6 +102,7 @@ class MelnorZoneNumber(MelnorZoneEntity, NumberEntity):
|
||||||
"""A number implementation for a melnor device."""
|
"""A number implementation for a melnor device."""
|
||||||
|
|
||||||
entity_description: MelnorZoneNumberEntityDescription
|
entity_description: MelnorZoneNumberEntityDescription
|
||||||
|
_attr_mode = NumberMode.BOX
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -88,7 +116,7 @@ class MelnorZoneNumber(MelnorZoneEntity, NumberEntity):
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> float | None:
|
def native_value(self) -> float | None:
|
||||||
"""Return the current value."""
|
"""Return the current value."""
|
||||||
return self._valve.manual_watering_minutes
|
return self.entity_description.state_fn(self._valve)
|
||||||
|
|
||||||
async def async_set_native_value(self, value: float) -> None:
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
"""Update the current value."""
|
"""Update the current value."""
|
||||||
|
|
|
@ -45,6 +45,15 @@ def watering_seconds_left(valve: Valve) -> datetime | None:
|
||||||
return dt_util.utc_from_timestamp(valve.watering_end_time)
|
return dt_util.utc_from_timestamp(valve.watering_end_time)
|
||||||
|
|
||||||
|
|
||||||
|
def next_cycle(valve: Valve) -> datetime | None:
|
||||||
|
"""Return the value of the next_cycle date, only if the cycle is enabled."""
|
||||||
|
|
||||||
|
if valve.schedule_enabled is True:
|
||||||
|
return valve.next_cycle
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MelnorSensorEntityDescriptionMixin:
|
class MelnorSensorEntityDescriptionMixin:
|
||||||
"""Mixin for required keys."""
|
"""Mixin for required keys."""
|
||||||
|
@ -102,6 +111,12 @@ ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
|
||||||
name="Manual Cycle End",
|
name="Manual Cycle End",
|
||||||
state_fn=watering_seconds_left,
|
state_fn=watering_seconds_left,
|
||||||
),
|
),
|
||||||
|
MelnorZoneSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
key="next_cycle",
|
||||||
|
name="Next Cycle",
|
||||||
|
state_fn=next_cycle,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,15 @@ ZONE_ENTITY_DESCRIPTIONS = [
|
||||||
key="manual",
|
key="manual",
|
||||||
on_off_fn=lambda valve, bool: valve.set_is_watering(bool),
|
on_off_fn=lambda valve, bool: valve.set_is_watering(bool),
|
||||||
state_fn=lambda valve: valve.is_watering,
|
state_fn=lambda valve: valve.is_watering,
|
||||||
)
|
),
|
||||||
|
MelnorSwitchEntityDescription(
|
||||||
|
device_class=SwitchDeviceClass.SWITCH,
|
||||||
|
icon="mdi:calendar-sync-outline",
|
||||||
|
key="frequency",
|
||||||
|
name="Schedule",
|
||||||
|
on_off_fn=lambda valve, bool: valve.set_frequency_enabled(bool),
|
||||||
|
state_fn=lambda valve: valve.schedule_enabled,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1121,7 +1121,7 @@ mcstatus==6.0.0
|
||||||
meater-python==0.0.8
|
meater-python==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.melnor
|
# homeassistant.components.melnor
|
||||||
melnor-bluetooth==0.0.20
|
melnor-bluetooth==0.0.21
|
||||||
|
|
||||||
# homeassistant.components.message_bird
|
# homeassistant.components.message_bird
|
||||||
messagebird==1.2.0
|
messagebird==1.2.0
|
||||||
|
|
|
@ -847,7 +847,7 @@ mcstatus==6.0.0
|
||||||
meater-python==0.0.8
|
meater-python==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.melnor
|
# homeassistant.components.melnor
|
||||||
melnor-bluetooth==0.0.20
|
melnor-bluetooth==0.0.21
|
||||||
|
|
||||||
# homeassistant.components.meteo_france
|
# homeassistant.components.meteo_france
|
||||||
meteofrance-api==1.2.0
|
meteofrance-api==1.2.0
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
from datetime import datetime, time, timedelta, timezone
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from melnor_bluetooth.device import Device
|
from melnor_bluetooth.device import Device
|
||||||
|
@ -57,13 +58,78 @@ def mock_bluetooth(enable_bluetooth):
|
||||||
"""Auto mock bluetooth."""
|
"""Auto mock bluetooth."""
|
||||||
|
|
||||||
|
|
||||||
class MockedValve:
|
class MockFrequency:
|
||||||
|
"""Mocked class for a Frequency."""
|
||||||
|
|
||||||
|
_duration: int
|
||||||
|
_interval: int
|
||||||
|
_is_watering: bool
|
||||||
|
_start_time: time
|
||||||
|
_next_run_time: datetime
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize a mocked frequency."""
|
||||||
|
self._duration = 0
|
||||||
|
self._interval = 0
|
||||||
|
self._is_watering = False
|
||||||
|
self._start_time = time(12, 0)
|
||||||
|
self._next_run_time = datetime(2021, 1, 1, 12, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duration_minutes(self) -> int:
|
||||||
|
"""Return the duration in minutes."""
|
||||||
|
return self._duration
|
||||||
|
|
||||||
|
@duration_minutes.setter
|
||||||
|
def duration_minutes(self, duration: int) -> None:
|
||||||
|
"""Set the duration in minutes."""
|
||||||
|
self._duration = duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def interval_hours(self) -> int:
|
||||||
|
"""Return the interval in hours."""
|
||||||
|
return self._interval
|
||||||
|
|
||||||
|
@interval_hours.setter
|
||||||
|
def interval_hours(self, interval: int) -> None:
|
||||||
|
"""Set the interval in hours."""
|
||||||
|
self._interval = interval
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_time(self) -> time:
|
||||||
|
"""Return the start time."""
|
||||||
|
return self._start_time
|
||||||
|
|
||||||
|
@start_time.setter
|
||||||
|
def start_time(self, start_time: time) -> None:
|
||||||
|
"""Set the start time."""
|
||||||
|
self._start_time = start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_watering(self) -> bool:
|
||||||
|
"""Return true if the frequency is currently watering."""
|
||||||
|
return self._is_watering
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_run_time(self) -> datetime:
|
||||||
|
"""Return the next run time."""
|
||||||
|
return self._next_run_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schedule_end_time(self) -> datetime:
|
||||||
|
"""Return the schedule end time."""
|
||||||
|
return self._next_run_time + timedelta(minutes=self._duration)
|
||||||
|
|
||||||
|
|
||||||
|
class MockValve:
|
||||||
"""Mocked class for a Valve."""
|
"""Mocked class for a Valve."""
|
||||||
|
|
||||||
_id: int
|
_id: int
|
||||||
_is_watering: bool
|
_is_watering: bool
|
||||||
_manual_watering_minutes: int
|
_manual_watering_minutes: int
|
||||||
_end_time: int
|
_end_time: int
|
||||||
|
_frequency: MockFrequency
|
||||||
|
_schedule_enabled: bool
|
||||||
|
|
||||||
def __init__(self, identifier: int) -> None:
|
def __init__(self, identifier: int) -> None:
|
||||||
"""Initialize a mocked valve."""
|
"""Initialize a mocked valve."""
|
||||||
|
@ -71,35 +137,69 @@ class MockedValve:
|
||||||
self._id = identifier
|
self._id = identifier
|
||||||
self._is_watering = False
|
self._is_watering = False
|
||||||
self._manual_watering_minutes = 0
|
self._manual_watering_minutes = 0
|
||||||
|
self._schedule_enabled = False
|
||||||
|
|
||||||
|
self._frequency = MockFrequency()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> int:
|
def id(self) -> int:
|
||||||
"""Return the valve id."""
|
"""Return the valve id."""
|
||||||
return self._id
|
return self._id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frequency(self):
|
||||||
|
"""Return the frequency."""
|
||||||
|
return self._frequency
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_watering(self):
|
def is_watering(self):
|
||||||
"""Return true if the valve is currently watering."""
|
"""Return true if the valve is currently watering."""
|
||||||
return self._is_watering
|
return self._is_watering
|
||||||
|
|
||||||
async def set_is_watering(self, is_watering: bool):
|
|
||||||
"""Set the valve to manual watering."""
|
|
||||||
self._is_watering = is_watering
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def manual_watering_minutes(self):
|
def manual_watering_minutes(self):
|
||||||
"""Return the number of minutes the valve is set to manual watering."""
|
"""Return the number of minutes the valve is set to manual watering."""
|
||||||
return self._manual_watering_minutes
|
return self._manual_watering_minutes
|
||||||
|
|
||||||
async def set_manual_watering_minutes(self, minutes: int):
|
@property
|
||||||
"""Set the valve to manual watering."""
|
def next_cycle(self):
|
||||||
self._manual_watering_minutes = minutes
|
"""Return the end time of the current watering cycle."""
|
||||||
|
return self._frequency.next_run_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schedule_enabled(self) -> bool:
|
||||||
|
"""Return true if the schedule is enabled."""
|
||||||
|
return self._schedule_enabled
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def watering_end_time(self) -> int:
|
def watering_end_time(self) -> int:
|
||||||
"""Return the end time of the current watering cycle."""
|
"""Return the end time of the current watering cycle."""
|
||||||
return self._end_time
|
return self._end_time
|
||||||
|
|
||||||
|
async def set_is_watering(self, is_watering: bool):
|
||||||
|
"""Set the valve to manual watering."""
|
||||||
|
self._is_watering = is_watering
|
||||||
|
|
||||||
|
async def set_manual_watering_minutes(self, minutes: int):
|
||||||
|
"""Set the valve to manual watering."""
|
||||||
|
self._manual_watering_minutes = minutes
|
||||||
|
|
||||||
|
async def set_frequency_interval_hours(self, interval: int):
|
||||||
|
"""Set the frequency interval in hours."""
|
||||||
|
self._frequency.interval_hours = interval
|
||||||
|
|
||||||
|
async def set_frequency_duration_minutes(self, duration: int):
|
||||||
|
"""Set the frequency duration in minutes."""
|
||||||
|
self._frequency.duration_minutes = duration
|
||||||
|
|
||||||
|
async def set_frequency_enabled(self, enabled: bool):
|
||||||
|
"""Set the frequency schedule enabled."""
|
||||||
|
self._schedule_enabled = enabled
|
||||||
|
|
||||||
|
async def set_frequency_start_time(self, value: time):
|
||||||
|
"""Set the frequency schedule enabled."""
|
||||||
|
self._frequency.start_time = value
|
||||||
|
|
||||||
|
|
||||||
def mock_config_entry(hass: HomeAssistant):
|
def mock_config_entry(hass: HomeAssistant):
|
||||||
"""Return a mock config entry."""
|
"""Return a mock config entry."""
|
||||||
|
@ -131,10 +231,10 @@ def mock_melnor_device():
|
||||||
device.name = "test_melnor"
|
device.name = "test_melnor"
|
||||||
device.rssi = -50
|
device.rssi = -50
|
||||||
|
|
||||||
device.zone1 = MockedValve(0)
|
device.zone1 = MockValve(0)
|
||||||
device.zone2 = MockedValve(1)
|
device.zone2 = MockValve(1)
|
||||||
device.zone3 = MockedValve(2)
|
device.zone3 = MockValve(2)
|
||||||
device.zone4 = MockedValve(3)
|
device.zone4 = MockValve(3)
|
||||||
|
|
||||||
device.__getitem__.side_effect = lambda key: getattr(device, key)
|
device.__getitem__.side_effect = lambda key: getattr(device, key)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from .conftest import (
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_watering_minutes(hass: HomeAssistant) -> None:
|
async def test_manual_watering_minutes(hass: HomeAssistant) -> None:
|
||||||
"""Test the manual watering switch."""
|
"""Test the manual watering duration number."""
|
||||||
|
|
||||||
entry = mock_config_entry(hass)
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
|
@ -22,8 +22,9 @@ async def test_manual_watering_minutes(hass: HomeAssistant) -> None:
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
number = hass.states.get("number.zone_1_manual_minutes")
|
number = hass.states.get("number.zone_1_manual_duration")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
assert number.state == "0"
|
assert number.state == "0"
|
||||||
assert number.attributes["max"] == 360
|
assert number.attributes["max"] == 360
|
||||||
assert number.attributes["min"] == 1
|
assert number.attributes["min"] == 1
|
||||||
|
@ -35,11 +36,84 @@ async def test_manual_watering_minutes(hass: HomeAssistant) -> None:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"number",
|
"number",
|
||||||
"set_value",
|
"set_value",
|
||||||
{"entity_id": "number.zone_1_manual_minutes", "value": 10},
|
{"entity_id": "number.zone_1_manual_duration", "value": 10},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
number = hass.states.get("number.zone_1_manual_minutes")
|
number = hass.states.get("number.zone_1_manual_duration")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
assert number.state == "10"
|
assert number.state == "10"
|
||||||
assert device.zone1.manual_watering_minutes == 10
|
assert device.zone1.manual_watering_minutes == 10
|
||||||
|
|
||||||
|
|
||||||
|
async def test_frequency_interval_hours(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the interval hours number."""
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
|
with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback():
|
||||||
|
device = device_patch.return_value
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_schedule_interval")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
|
assert number.state == "0"
|
||||||
|
assert number.attributes["max"] == 168
|
||||||
|
assert number.attributes["min"] == 1
|
||||||
|
assert number.attributes["step"] == 1.0
|
||||||
|
assert number.attributes["icon"] == "mdi:calendar-refresh-outline"
|
||||||
|
|
||||||
|
assert device.zone1.frequency.interval_hours == 0
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": "number.zone_1_schedule_interval", "value": 10},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_schedule_interval")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
|
assert number.state == "10"
|
||||||
|
assert device.zone1.frequency.interval_hours == 10
|
||||||
|
|
||||||
|
|
||||||
|
async def test_frequency_duration_minutes(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the duration minutes number."""
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
|
with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback():
|
||||||
|
device = device_patch.return_value
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_schedule_duration")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
|
assert number.state == "0"
|
||||||
|
assert number.attributes["max"] == 360
|
||||||
|
assert number.attributes["min"] == 1
|
||||||
|
assert number.attributes["step"] == 1.0
|
||||||
|
assert number.attributes["icon"] == "mdi:timer-outline"
|
||||||
|
|
||||||
|
assert device.zone1.frequency.duration_minutes == 0
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": "number.zone_1_schedule_duration", "value": 10},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_schedule_duration")
|
||||||
|
|
||||||
|
assert number is not None
|
||||||
|
assert number.state == "10"
|
||||||
|
assert device.zone1.frequency.duration_minutes == 10
|
||||||
|
|
|
@ -30,6 +30,8 @@ async def test_battery_sensor(hass: HomeAssistant) -> None:
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
battery_sensor = hass.states.get("sensor.test_melnor_battery")
|
battery_sensor = hass.states.get("sensor.test_melnor_battery")
|
||||||
|
|
||||||
|
assert battery_sensor is not None
|
||||||
assert battery_sensor.state == "80"
|
assert battery_sensor.state == "80"
|
||||||
assert battery_sensor.attributes["unit_of_measurement"] == PERCENTAGE
|
assert battery_sensor.attributes["unit_of_measurement"] == PERCENTAGE
|
||||||
assert battery_sensor.attributes["device_class"] == SensorDeviceClass.BATTERY
|
assert battery_sensor.attributes["device_class"] == SensorDeviceClass.BATTERY
|
||||||
|
@ -58,6 +60,8 @@ async def test_minutes_remaining_sensor(hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
# Valve is off, report 0
|
# Valve is off, report 0
|
||||||
minutes_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
minutes_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
||||||
|
|
||||||
|
assert minutes_sensor is not None
|
||||||
assert minutes_sensor.state == "unknown"
|
assert minutes_sensor.state == "unknown"
|
||||||
assert minutes_sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
assert minutes_sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
@ -69,9 +73,50 @@ async def test_minutes_remaining_sensor(hass: HomeAssistant) -> None:
|
||||||
|
|
||||||
# Valve is on, report 10
|
# Valve is on, report 10
|
||||||
minutes_remaining_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
minutes_remaining_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
||||||
|
|
||||||
|
assert minutes_remaining_sensor is not None
|
||||||
assert minutes_remaining_sensor.state == end_time.isoformat(timespec="seconds")
|
assert minutes_remaining_sensor.state == end_time.isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_schedule_next_cycle_sensor(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the frequency next_cycle sensor."""
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
device = mock_melnor_device()
|
||||||
|
|
||||||
|
next_cycle = now + dt_util.dt.timedelta(minutes=10)
|
||||||
|
|
||||||
|
# we control this mock
|
||||||
|
device.zone1.frequency._next_run_time = next_cycle
|
||||||
|
|
||||||
|
with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device(
|
||||||
|
device
|
||||||
|
), patch_async_register_callback():
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Valve is off, report 0
|
||||||
|
minutes_sensor = hass.states.get("sensor.zone_1_next_cycle")
|
||||||
|
|
||||||
|
assert minutes_sensor is not None
|
||||||
|
assert minutes_sensor.state == "unknown"
|
||||||
|
assert minutes_sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
# Turn valve on
|
||||||
|
device.zone1._schedule_enabled = True
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Valve is on, report 10
|
||||||
|
next_cycle_sensor = hass.states.get("sensor.zone_1_next_cycle")
|
||||||
|
|
||||||
|
assert next_cycle_sensor is not None
|
||||||
|
assert next_cycle_sensor.state == next_cycle.isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
async def test_rssi_sensor(
|
async def test_rssi_sensor(
|
||||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -104,6 +149,7 @@ async def test_rssi_sensor(
|
||||||
|
|
||||||
rssi = hass.states.get(entity_id)
|
rssi = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert rssi is not None
|
||||||
assert (
|
assert (
|
||||||
rssi.attributes["unit_of_measurement"] == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
rssi.attributes["unit_of_measurement"] == SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,6 +23,8 @@ async def test_manual_watering_switch_metadata(hass: HomeAssistant) -> None:
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1")
|
switch = hass.states.get("switch.zone_1")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH
|
assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH
|
||||||
assert switch.attributes["icon"] == "mdi:sprinkler"
|
assert switch.attributes["icon"] == "mdi:sprinkler"
|
||||||
|
|
||||||
|
@ -39,6 +41,8 @@ async def test_manual_watering_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1")
|
switch = hass.states.get("switch.zone_1")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
assert switch.state is STATE_OFF
|
assert switch.state is STATE_OFF
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -49,6 +53,8 @@ async def test_manual_watering_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1")
|
switch = hass.states.get("switch.zone_1")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
assert switch.state is STATE_ON
|
assert switch.state is STATE_ON
|
||||||
assert device.zone1.is_watering is True
|
assert device.zone1.is_watering is True
|
||||||
|
|
||||||
|
@ -60,5 +66,38 @@ async def test_manual_watering_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1")
|
switch = hass.states.get("switch.zone_1")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
assert switch.state is STATE_OFF
|
assert switch.state is STATE_OFF
|
||||||
assert device.zone1.is_watering is False
|
assert device.zone1.is_watering is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_schedule_enabled_switch_on_off(hass: HomeAssistant) -> None:
|
||||||
|
"""Test the schedule enabled switch."""
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
|
with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback():
|
||||||
|
device = device_patch.return_value
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
switch = hass.states.get("switch.zone_1_schedule")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
|
assert switch.state is STATE_OFF
|
||||||
|
assert device.zone1.schedule_enabled is False
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch",
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": "switch.zone_1_schedule"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
switch = hass.states.get("switch.zone_1_schedule")
|
||||||
|
|
||||||
|
assert switch is not None
|
||||||
|
assert switch.state is STATE_ON
|
||||||
|
assert device.zone1.schedule_enabled is True
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue