mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Add smarttub cover sensor (#139134)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
a069b59efc
commit
b2710c1bce
@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from smarttub import Spa, SpaError, SpaReminder
|
||||
@ -17,9 +18,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import ATTR_ERRORS, ATTR_REMINDERS
|
||||
from .const import ATTR_ERRORS, ATTR_REMINDERS, ATTR_SENSORS
|
||||
from .controller import SmartTubConfigEntry
|
||||
from .entity import SmartTubEntity, SmartTubSensorBase
|
||||
from .entity import (
|
||||
SmartTubEntity,
|
||||
SmartTubExternalSensorBase,
|
||||
SmartTubOnboardSensorBase,
|
||||
)
|
||||
|
||||
# whether the reminder has been snoozed (bool)
|
||||
ATTR_REMINDER_SNOOZED = "snoozed"
|
||||
@ -44,6 +49,8 @@ SNOOZE_REMINDER_SCHEMA: VolDictType = {
|
||||
)
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -62,6 +69,12 @@ async def async_setup_entry(
|
||||
SmartTubReminder(controller.coordinator, spa, reminder)
|
||||
for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values()
|
||||
)
|
||||
for sensor in controller.coordinator.data[spa.id][ATTR_SENSORS].values():
|
||||
name = sensor.name.strip("{}")
|
||||
if name.startswith("cover-"):
|
||||
entities.append(
|
||||
SmartTubCoverSensor(controller.coordinator, spa, sensor)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@ -79,7 +92,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class SmartTubOnline(SmartTubSensorBase, BinarySensorEntity):
|
||||
class SmartTubOnline(SmartTubOnboardSensorBase, BinarySensorEntity):
|
||||
"""A binary sensor indicating whether the spa is currently online (connected to the cloud)."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
@ -192,3 +205,16 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity):
|
||||
ATTR_CREATED_AT: error.created_at.isoformat(),
|
||||
ATTR_UPDATED_AT: error.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
class SmartTubCoverSensor(SmartTubExternalSensorBase, BinarySensorEntity):
|
||||
"""Wireless magnetic cover sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.OPENING
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return False if the cover is closed, True if open."""
|
||||
# magnet is True when the cover is closed, False when open
|
||||
# device class OPENING wants True to mean open, False to mean closed
|
||||
return not self.sensor.magnet
|
||||
|
@ -24,3 +24,4 @@ ATTR_LIGHTS = "lights"
|
||||
ATTR_PUMPS = "pumps"
|
||||
ATTR_REMINDERS = "reminders"
|
||||
ATTR_STATUS = "status"
|
||||
ATTR_SENSORS = "sensors"
|
||||
|
@ -22,6 +22,7 @@ from .const import (
|
||||
ATTR_LIGHTS,
|
||||
ATTR_PUMPS,
|
||||
ATTR_REMINDERS,
|
||||
ATTR_SENSORS,
|
||||
ATTR_STATUS,
|
||||
DOMAIN,
|
||||
POLLING_TIMEOUT,
|
||||
@ -108,6 +109,7 @@ class SmartTubController:
|
||||
ATTR_LIGHTS: {light.zone: light for light in full_status.lights},
|
||||
ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders},
|
||||
ATTR_ERRORS: errors,
|
||||
ATTR_SENSORS: {sensor.address: sensor for sensor in full_status.sensors},
|
||||
}
|
||||
|
||||
@callback
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from smarttub import Spa, SpaState
|
||||
from smarttub import Spa, SpaSensor, SpaState
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import (
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import ATTR_SENSORS, DOMAIN
|
||||
from .helpers import get_spa_name
|
||||
|
||||
|
||||
@ -47,8 +47,8 @@ class SmartTubEntity(CoordinatorEntity):
|
||||
return self.coordinator.data[self.spa.id].get("status")
|
||||
|
||||
|
||||
class SmartTubSensorBase(SmartTubEntity):
|
||||
"""Base class for SmartTub sensors."""
|
||||
class SmartTubOnboardSensorBase(SmartTubEntity):
|
||||
"""Base class for SmartTub onboard sensors."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -65,3 +65,29 @@ class SmartTubSensorBase(SmartTubEntity):
|
||||
def _state(self):
|
||||
"""Retrieve the underlying state from the spa."""
|
||||
return getattr(self.spa_status, self._state_key)
|
||||
|
||||
|
||||
class SmartTubExternalSensorBase(SmartTubEntity):
|
||||
"""Class for additional BLE wireless sensors sold separately."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: DataUpdateCoordinator[dict[str, Any]],
|
||||
spa: Spa,
|
||||
sensor: SpaSensor,
|
||||
) -> None:
|
||||
"""Initialize the external sensor entity."""
|
||||
self.sensor_address = sensor.address
|
||||
self._attr_unique_id = f"{spa.id}-externalsensor-{sensor.address}"
|
||||
super().__init__(coordinator, spa, self._human_readable_name(sensor))
|
||||
|
||||
@staticmethod
|
||||
def _human_readable_name(sensor: SpaSensor) -> str:
|
||||
return " ".join(
|
||||
word.capitalize() for word in sensor.name.strip("{}").split("-")
|
||||
)
|
||||
|
||||
@property
|
||||
def sensor(self) -> SpaSensor:
|
||||
"""Convenience property to access the smarttub.SpaSensor instance for this sensor."""
|
||||
return self.coordinator.data[self.spa.id][ATTR_SENSORS][self.sensor_address]
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers.typing import VolDictType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .controller import SmartTubConfigEntry
|
||||
from .entity import SmartTubSensorBase
|
||||
from .entity import SmartTubOnboardSensorBase
|
||||
|
||||
# the desired duration, in hours, of the cycle
|
||||
ATTR_DURATION = "duration"
|
||||
@ -56,16 +56,16 @@ async def async_setup_entry(
|
||||
for spa in controller.spas:
|
||||
entities.extend(
|
||||
[
|
||||
SmartTubSensor(controller.coordinator, spa, "State", "state"),
|
||||
SmartTubSensor(
|
||||
SmartTubBuiltinSensor(controller.coordinator, spa, "State", "state"),
|
||||
SmartTubBuiltinSensor(
|
||||
controller.coordinator, spa, "Flow Switch", "flow_switch"
|
||||
),
|
||||
SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"),
|
||||
SmartTubSensor(controller.coordinator, spa, "UV", "uv"),
|
||||
SmartTubSensor(
|
||||
SmartTubBuiltinSensor(controller.coordinator, spa, "Ozone", "ozone"),
|
||||
SmartTubBuiltinSensor(controller.coordinator, spa, "UV", "uv"),
|
||||
SmartTubBuiltinSensor(
|
||||
controller.coordinator, spa, "Blowout Cycle", "blowout_cycle"
|
||||
),
|
||||
SmartTubSensor(
|
||||
SmartTubBuiltinSensor(
|
||||
controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle"
|
||||
),
|
||||
SmartTubPrimaryFiltrationCycle(controller.coordinator, spa),
|
||||
@ -90,7 +90,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class SmartTubSensor(SmartTubSensorBase, SensorEntity):
|
||||
class SmartTubBuiltinSensor(SmartTubOnboardSensorBase, SensorEntity):
|
||||
"""Generic class for SmartTub status sensors."""
|
||||
|
||||
@property
|
||||
@ -105,7 +105,7 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity):
|
||||
return self._state.lower()
|
||||
|
||||
|
||||
class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
|
||||
class SmartTubPrimaryFiltrationCycle(SmartTubBuiltinSensor):
|
||||
"""The primary filtration cycle."""
|
||||
|
||||
def __init__(
|
||||
@ -145,7 +145,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
|
||||
class SmartTubSecondaryFiltrationCycle(SmartTubBuiltinSensor):
|
||||
"""The secondary filtration cycle."""
|
||||
|
||||
def __init__(
|
||||
|
@ -81,6 +81,16 @@ def mock_spa(spa_state):
|
||||
|
||||
spa_state.lights = [mock_light_off, mock_light_on]
|
||||
|
||||
mock_cover_sensor = create_autospec(smarttub.SpaSensor, instance=True)
|
||||
mock_cover_sensor.spa = mock_spa
|
||||
mock_cover_sensor.address = "address1"
|
||||
mock_cover_sensor.name = "{cover-sensor-1}"
|
||||
mock_cover_sensor.type = "ibs0x"
|
||||
mock_cover_sensor.subType = "magnet"
|
||||
mock_cover_sensor.magnet = True # closed
|
||||
|
||||
spa_state.sensors = [mock_cover_sensor]
|
||||
|
||||
mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True)
|
||||
mock_filter_reminder.id = "FILTER01"
|
||||
mock_filter_reminder.name = "MyFilter"
|
||||
@ -127,6 +137,7 @@ def mock_spa_state():
|
||||
"cleanupCycle": "INACTIVE",
|
||||
"lights": [],
|
||||
"pumps": [],
|
||||
"sensors": [],
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -104,3 +104,14 @@ async def test_reset_reminder(spa, setup_entry, hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
reminder.reset.assert_called_with(days)
|
||||
|
||||
|
||||
async def test_cover_sensor(hass: HomeAssistant, spa, setup_entry) -> None:
|
||||
"""Test cover sensor."""
|
||||
|
||||
entity_id = f"binary_sensor.{spa.brand}_{spa.model}_cover_sensor_1"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
assert state.state == STATE_OFF # closed
|
||||
|
Loading…
x
Reference in New Issue
Block a user