mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from smarttub import Spa, SpaError, SpaReminder
|
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.typing import VolDictType
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
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 .controller import SmartTubConfigEntry
|
||||||
from .entity import SmartTubEntity, SmartTubSensorBase
|
from .entity import (
|
||||||
|
SmartTubEntity,
|
||||||
|
SmartTubExternalSensorBase,
|
||||||
|
SmartTubOnboardSensorBase,
|
||||||
|
)
|
||||||
|
|
||||||
# whether the reminder has been snoozed (bool)
|
# whether the reminder has been snoozed (bool)
|
||||||
ATTR_REMINDER_SNOOZED = "snoozed"
|
ATTR_REMINDER_SNOOZED = "snoozed"
|
||||||
@ -44,6 +49,8 @@ SNOOZE_REMINDER_SCHEMA: VolDictType = {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -62,6 +69,12 @@ async def async_setup_entry(
|
|||||||
SmartTubReminder(controller.coordinator, spa, reminder)
|
SmartTubReminder(controller.coordinator, spa, reminder)
|
||||||
for reminder in controller.coordinator.data[spa.id][ATTR_REMINDERS].values()
|
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)
|
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)."""
|
"""A binary sensor indicating whether the spa is currently online (connected to the cloud)."""
|
||||||
|
|
||||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||||
@ -192,3 +205,16 @@ class SmartTubError(SmartTubEntity, BinarySensorEntity):
|
|||||||
ATTR_CREATED_AT: error.created_at.isoformat(),
|
ATTR_CREATED_AT: error.created_at.isoformat(),
|
||||||
ATTR_UPDATED_AT: error.updated_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_PUMPS = "pumps"
|
||||||
ATTR_REMINDERS = "reminders"
|
ATTR_REMINDERS = "reminders"
|
||||||
ATTR_STATUS = "status"
|
ATTR_STATUS = "status"
|
||||||
|
ATTR_SENSORS = "sensors"
|
||||||
|
@ -22,6 +22,7 @@ from .const import (
|
|||||||
ATTR_LIGHTS,
|
ATTR_LIGHTS,
|
||||||
ATTR_PUMPS,
|
ATTR_PUMPS,
|
||||||
ATTR_REMINDERS,
|
ATTR_REMINDERS,
|
||||||
|
ATTR_SENSORS,
|
||||||
ATTR_STATUS,
|
ATTR_STATUS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
POLLING_TIMEOUT,
|
POLLING_TIMEOUT,
|
||||||
@ -108,6 +109,7 @@ class SmartTubController:
|
|||||||
ATTR_LIGHTS: {light.zone: light for light in full_status.lights},
|
ATTR_LIGHTS: {light.zone: light for light in full_status.lights},
|
||||||
ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders},
|
ATTR_REMINDERS: {reminder.id: reminder for reminder in reminders},
|
||||||
ATTR_ERRORS: errors,
|
ATTR_ERRORS: errors,
|
||||||
|
ATTR_SENSORS: {sensor.address: sensor for sensor in full_status.sensors},
|
||||||
}
|
}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
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.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
DataUpdateCoordinator,
|
DataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import ATTR_SENSORS, DOMAIN
|
||||||
from .helpers import get_spa_name
|
from .helpers import get_spa_name
|
||||||
|
|
||||||
|
|
||||||
@ -47,8 +47,8 @@ class SmartTubEntity(CoordinatorEntity):
|
|||||||
return self.coordinator.data[self.spa.id].get("status")
|
return self.coordinator.data[self.spa.id].get("status")
|
||||||
|
|
||||||
|
|
||||||
class SmartTubSensorBase(SmartTubEntity):
|
class SmartTubOnboardSensorBase(SmartTubEntity):
|
||||||
"""Base class for SmartTub sensors."""
|
"""Base class for SmartTub onboard sensors."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -65,3 +65,29 @@ class SmartTubSensorBase(SmartTubEntity):
|
|||||||
def _state(self):
|
def _state(self):
|
||||||
"""Retrieve the underlying state from the spa."""
|
"""Retrieve the underlying state from the spa."""
|
||||||
return getattr(self.spa_status, self._state_key)
|
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .controller import SmartTubConfigEntry
|
from .controller import SmartTubConfigEntry
|
||||||
from .entity import SmartTubSensorBase
|
from .entity import SmartTubOnboardSensorBase
|
||||||
|
|
||||||
# the desired duration, in hours, of the cycle
|
# the desired duration, in hours, of the cycle
|
||||||
ATTR_DURATION = "duration"
|
ATTR_DURATION = "duration"
|
||||||
@ -56,16 +56,16 @@ async def async_setup_entry(
|
|||||||
for spa in controller.spas:
|
for spa in controller.spas:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
[
|
||||||
SmartTubSensor(controller.coordinator, spa, "State", "state"),
|
SmartTubBuiltinSensor(controller.coordinator, spa, "State", "state"),
|
||||||
SmartTubSensor(
|
SmartTubBuiltinSensor(
|
||||||
controller.coordinator, spa, "Flow Switch", "flow_switch"
|
controller.coordinator, spa, "Flow Switch", "flow_switch"
|
||||||
),
|
),
|
||||||
SmartTubSensor(controller.coordinator, spa, "Ozone", "ozone"),
|
SmartTubBuiltinSensor(controller.coordinator, spa, "Ozone", "ozone"),
|
||||||
SmartTubSensor(controller.coordinator, spa, "UV", "uv"),
|
SmartTubBuiltinSensor(controller.coordinator, spa, "UV", "uv"),
|
||||||
SmartTubSensor(
|
SmartTubBuiltinSensor(
|
||||||
controller.coordinator, spa, "Blowout Cycle", "blowout_cycle"
|
controller.coordinator, spa, "Blowout Cycle", "blowout_cycle"
|
||||||
),
|
),
|
||||||
SmartTubSensor(
|
SmartTubBuiltinSensor(
|
||||||
controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle"
|
controller.coordinator, spa, "Cleanup Cycle", "cleanup_cycle"
|
||||||
),
|
),
|
||||||
SmartTubPrimaryFiltrationCycle(controller.coordinator, spa),
|
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."""
|
"""Generic class for SmartTub status sensors."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -105,7 +105,7 @@ class SmartTubSensor(SmartTubSensorBase, SensorEntity):
|
|||||||
return self._state.lower()
|
return self._state.lower()
|
||||||
|
|
||||||
|
|
||||||
class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
|
class SmartTubPrimaryFiltrationCycle(SmartTubBuiltinSensor):
|
||||||
"""The primary filtration cycle."""
|
"""The primary filtration cycle."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -145,7 +145,7 @@ class SmartTubPrimaryFiltrationCycle(SmartTubSensor):
|
|||||||
await self.coordinator.async_request_refresh()
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
|
||||||
class SmartTubSecondaryFiltrationCycle(SmartTubSensor):
|
class SmartTubSecondaryFiltrationCycle(SmartTubBuiltinSensor):
|
||||||
"""The secondary filtration cycle."""
|
"""The secondary filtration cycle."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -81,6 +81,16 @@ def mock_spa(spa_state):
|
|||||||
|
|
||||||
spa_state.lights = [mock_light_off, mock_light_on]
|
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 = create_autospec(smarttub.SpaReminder, instance=True)
|
||||||
mock_filter_reminder.id = "FILTER01"
|
mock_filter_reminder.id = "FILTER01"
|
||||||
mock_filter_reminder.name = "MyFilter"
|
mock_filter_reminder.name = "MyFilter"
|
||||||
@ -127,6 +137,7 @@ def mock_spa_state():
|
|||||||
"cleanupCycle": "INACTIVE",
|
"cleanupCycle": "INACTIVE",
|
||||||
"lights": [],
|
"lights": [],
|
||||||
"pumps": [],
|
"pumps": [],
|
||||||
|
"sensors": [],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,3 +104,14 @@ async def test_reset_reminder(spa, setup_entry, hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
reminder.reset.assert_called_with(days)
|
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