diff --git a/homeassistant/components/watergate/__init__.py b/homeassistant/components/watergate/__init__.py index 1cf38876556d16..fa7611103395a7 100644 --- a/homeassistant/components/watergate/__init__.py +++ b/homeassistant/components/watergate/__init__.py @@ -25,8 +25,13 @@ _LOGGER = logging.getLogger(__name__) +WEBHOOK_TELEMETRY_TYPE = "telemetry" +WEBHOOK_VALVE_TYPE = "valve" +WEBHOOK_WIFI_CHANGED_TYPE = "wifi-changed" +WEBHOOK_POWER_SUPPLY_CHANGED_TYPE = "power-supply-changed" PLATFORMS: list[Platform] = [ + Platform.SENSOR, Platform.VALVE, ] @@ -82,7 +87,6 @@ def get_webhook_handler( async def async_webhook_handler( hass: HomeAssistant, webhook_id: str, request: Request ) -> Response | None: - # Handle http post calls to the path. if not request.body_exists: return HomeAssistantView.json( result="No Body", status_code=HTTPStatus.BAD_REQUEST @@ -96,9 +100,29 @@ async def async_webhook_handler( body_type = body.get("type") - coordinator_data = coordinator.data - if body_type == Platform.VALVE and coordinator_data: - coordinator_data.valve_state = data.state + if not (coordinator_data := coordinator.data): + pass + elif body_type == WEBHOOK_VALVE_TYPE: + coordinator_data.state.valve_state = data.state + elif body_type == WEBHOOK_TELEMETRY_TYPE: + errors = data.errors or {} + coordinator_data.telemetry.flow = ( + data.flow if "flow" not in errors else None + ) + coordinator_data.telemetry.pressure = ( + data.pressure if "pressure" not in errors else None + ) + coordinator_data.telemetry.water_temperature = ( + data.temperature if "temperature" not in errors else None + ) + elif body_type == WEBHOOK_WIFI_CHANGED_TYPE: + coordinator_data.networking.ip = data.ip + coordinator_data.networking.gateway = data.gateway + coordinator_data.networking.subnet = data.subnet + coordinator_data.networking.ssid = data.ssid + coordinator_data.networking.rssi = data.rssi + elif body_type == WEBHOOK_POWER_SUPPLY_CHANGED_TYPE: + coordinator_data.state.power_supply = data.supply coordinator.async_set_updated_data(coordinator_data) diff --git a/homeassistant/components/watergate/coordinator.py b/homeassistant/components/watergate/coordinator.py index c0b87feed30ede..1d83b7a3ccb060 100644 --- a/homeassistant/components/watergate/coordinator.py +++ b/homeassistant/components/watergate/coordinator.py @@ -1,10 +1,11 @@ """Coordinator for Watergate API.""" +from dataclasses import dataclass from datetime import timedelta import logging from watergate_local_api import WatergateApiException, WatergateLocalApiClient -from watergate_local_api.models import DeviceState +from watergate_local_api.models import DeviceState, NetworkingData, TelemetryData from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -14,7 +15,16 @@ _LOGGER = logging.getLogger(__name__) -class WatergateDataCoordinator(DataUpdateCoordinator[DeviceState]): +@dataclass +class WatergateAgregatedRequests: + """Class to hold aggregated requests.""" + + state: DeviceState + telemetry: TelemetryData + networking: NetworkingData + + +class WatergateDataCoordinator(DataUpdateCoordinator[WatergateAgregatedRequests]): """Class to manage fetching watergate data.""" def __init__(self, hass: HomeAssistant, api: WatergateLocalApiClient) -> None: @@ -27,9 +37,22 @@ def __init__(self, hass: HomeAssistant, api: WatergateLocalApiClient) -> None: ) self.api = api - async def _async_update_data(self) -> DeviceState: + async def _async_update_data(self) -> WatergateAgregatedRequests: try: state = await self.api.async_get_device_state() + telemetry = await self.api.async_get_telemetry_data() + networking = await self.api.async_get_networking() except WatergateApiException as exc: - raise UpdateFailed from exc - return state + raise UpdateFailed(f"Sonic device is unavailable: {exc}") from exc + return WatergateAgregatedRequests(state, telemetry, networking) + + def async_set_updated_data(self, data: WatergateAgregatedRequests) -> None: + """Manually update data, notify listeners and DO NOT reset refresh interval.""" + + self.data = data + self.logger.debug( + "Manually updated %s data", + self.name, + ) + + self.async_update_listeners() diff --git a/homeassistant/components/watergate/entity.py b/homeassistant/components/watergate/entity.py index 977a7fbedb4506..8f43643029f2f2 100644 --- a/homeassistant/components/watergate/entity.py +++ b/homeassistant/components/watergate/entity.py @@ -20,11 +20,13 @@ def __init__( """Initialize the entity.""" super().__init__(coordinator) self._api_client = coordinator.api - self._attr_unique_id = f"{coordinator.data.serial_number}.{entity_name}" + self._attr_unique_id = f"{coordinator.data.state.serial_number}.{entity_name}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.data.serial_number)}, + identifiers={(DOMAIN, coordinator.data.state.serial_number)}, name="Sonic", - serial_number=coordinator.data.serial_number, + serial_number=coordinator.data.state.serial_number, manufacturer=MANUFACTURER, - sw_version=coordinator.data.firmware_version if coordinator.data else None, + sw_version=( + coordinator.data.state.firmware_version if coordinator.data else None + ), ) diff --git a/homeassistant/components/watergate/quality_scale.yaml b/homeassistant/components/watergate/quality_scale.yaml index c6027f6a54896f..b116eff970ed42 100644 --- a/homeassistant/components/watergate/quality_scale.yaml +++ b/homeassistant/components/watergate/quality_scale.yaml @@ -27,6 +27,7 @@ rules: test-before-configure: done test-before-setup: done unique-config-entry: done + # Silver config-entry-unloading: done log-when-unavailable: todo diff --git a/homeassistant/components/watergate/sensor.py b/homeassistant/components/watergate/sensor.py new file mode 100644 index 00000000000000..82ac7cfea92d42 --- /dev/null +++ b/homeassistant/components/watergate/sensor.py @@ -0,0 +1,214 @@ +"""Support for Watergate sensors.""" + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import StrEnum +import logging + +from homeassistant.components.sensor import ( + HomeAssistant, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + EntityCategory, + UnitOfPressure, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, + UnitOfVolumeFlowRate, +) +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util import dt as dt_util + +from . import WatergateConfigEntry +from .coordinator import WatergateAgregatedRequests, WatergateDataCoordinator +from .entity import WatergateEntity + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +class PowerSupplyMode(StrEnum): + """LED bar mode.""" + + BATTERY = "battery" + EXTERNAL = "external" + BATTERY_EXTERNAL = "battery_external" + + +@dataclass(kw_only=True, frozen=True) +class WatergateSensorEntityDescription(SensorEntityDescription): + """Description for Watergate sensor entities.""" + + value_fn: Callable[ + [WatergateAgregatedRequests], + StateType | datetime | PowerSupplyMode, + ] + + +DESCRIPTIONS: list[WatergateSensorEntityDescription] = [ + WatergateSensorEntityDescription( + value_fn=lambda data: ( + data.state.water_meter.duration + if data.state and data.state.water_meter + else None + ), + translation_key="water_meter_volume", + key="water_meter_volume", + native_unit_of_measurement=UnitOfVolume.LITERS, + device_class=SensorDeviceClass.WATER, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + data.state.water_meter.duration + if data.state and data.state.water_meter + else None + ), + translation_key="water_meter_duration", + key="water_meter_duration", + native_unit_of_measurement=UnitOfTime.MINUTES, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: data.networking.rssi if data.networking else None, + key="rssi", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + dt_util.as_utc( + dt_util.now() - timedelta(microseconds=data.networking.wifi_uptime) + ) + if data.networking + else None + ), + translation_key="wifi_up_since", + key="wifi_up_since", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + dt_util.as_utc( + dt_util.now() - timedelta(microseconds=data.networking.mqtt_uptime) + ) + if data.networking + else None + ), + translation_key="mqtt_up_since", + key="mqtt_up_since", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + data.telemetry.water_temperature if data.telemetry else None + ), + translation_key="water_temperature", + key="water_temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: data.telemetry.pressure if data.telemetry else None, + translation_key="water_pressure", + key="water_pressure", + native_unit_of_measurement=UnitOfPressure.MBAR, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + data.telemetry.flow / 1000 + if data.telemetry and data.telemetry.flow is not None + else None + ), + key="water_flow_rate", + native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + device_class=SensorDeviceClass.VOLUME_FLOW_RATE, + state_class=SensorStateClass.MEASUREMENT, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + dt_util.as_utc(dt_util.now() - timedelta(seconds=data.state.uptime)) + if data.state + else None + ), + translation_key="up_since", + key="up_since", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + ), + WatergateSensorEntityDescription( + value_fn=lambda data: ( + PowerSupplyMode(data.state.power_supply.replace("+", "_")) + if data.state + else None + ), + translation_key="power_supply_mode", + key="power_supply_mode", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.ENUM, + options=[member.value for member in PowerSupplyMode], + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: WatergateConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up all entries for Watergate Platform.""" + + coordinator = config_entry.runtime_data + + async_add_entities( + SonicSensor(coordinator, description) for description in DESCRIPTIONS + ) + + +class SonicSensor(WatergateEntity, SensorEntity): + """Define a Sonic Sensor entity.""" + + entity_description: WatergateSensorEntityDescription + + def __init__( + self, + coordinator: WatergateDataCoordinator, + entity_description: WatergateSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, entity_description.key) + self.entity_description = entity_description + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.value_fn(self.coordinator.data) is not None + ) + + @property + def native_value(self) -> str | int | float | datetime | PowerSupplyMode | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/watergate/strings.json b/homeassistant/components/watergate/strings.json index 2a75c4d103dfdf..c312525e420834 100644 --- a/homeassistant/components/watergate/strings.json +++ b/homeassistant/components/watergate/strings.json @@ -17,5 +17,38 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "sensor": { + "water_meter_volume": { + "name": "Water meter volume" + }, + "water_meter_duration": { + "name": "Water meter duration" + }, + "wifi_up_since": { + "name": "Wi-Fi up since" + }, + "mqtt_up_since": { + "name": "MQTT up since" + }, + "water_temperature": { + "name": "Water temperature" + }, + "water_pressure": { + "name": "Water pressure" + }, + "up_since": { + "name": "Up since" + }, + "power_supply_mode": { + "name": "Power supply mode", + "state": { + "battery": "Battery", + "external": "Mains", + "battery_external": "Battery and mains" + } + } + } } } diff --git a/homeassistant/components/watergate/valve.py b/homeassistant/components/watergate/valve.py index aecaf3fbca9702..556b53e1d3c56f 100644 --- a/homeassistant/components/watergate/valve.py +++ b/homeassistant/components/watergate/valve.py @@ -43,7 +43,9 @@ def __init__( ) -> None: """Initialize the sensor.""" super().__init__(coordinator, ENTITY_NAME) - self._valve_state = coordinator.data.valve_state if coordinator.data else None + self._valve_state = ( + coordinator.data.state.valve_state if coordinator.data.state else None + ) @property def is_closed(self) -> bool: @@ -65,7 +67,9 @@ def _handle_coordinator_update(self) -> None: """Handle data update.""" self._attr_available = self.coordinator.data is not None self._valve_state = ( - self.coordinator.data.valve_state if self.coordinator.data else None + self.coordinator.data.state.valve_state + if self.coordinator.data.state + else None ) self.async_write_ha_state() @@ -80,3 +84,8 @@ async def async_close_valve(self, **kwargs: Any) -> None: await self._api_client.async_set_valve_state(ValveState.CLOSED) self._valve_state = ValveState.CLOSING self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self.coordinator.data.state is not None diff --git a/tests/components/watergate/conftest.py b/tests/components/watergate/conftest.py index d29b90431a4c0c..6d40a4b715226b 100644 --- a/tests/components/watergate/conftest.py +++ b/tests/components/watergate/conftest.py @@ -9,7 +9,9 @@ from .const import ( DEFAULT_DEVICE_STATE, + DEFAULT_NETWORKING_STATE, DEFAULT_SERIAL_NUMBER, + DEFAULT_TELEMETRY_STATE, MOCK_CONFIG, MOCK_WEBHOOK_ID, ) @@ -35,6 +37,12 @@ def mock_watergate_client() -> Generator[AsyncMock]: mock_client_instance.async_get_device_state = AsyncMock( return_value=DEFAULT_DEVICE_STATE ) + mock_client_instance.async_get_networking = AsyncMock( + return_value=DEFAULT_NETWORKING_STATE + ) + mock_client_instance.async_get_telemetry_data = AsyncMock( + return_value=DEFAULT_TELEMETRY_STATE + ) yield mock_client_instance diff --git a/tests/components/watergate/const.py b/tests/components/watergate/const.py index 4297b3321ad77d..0f7cc12c14b537 100644 --- a/tests/components/watergate/const.py +++ b/tests/components/watergate/const.py @@ -1,6 +1,7 @@ """Constants for the Watergate tests.""" -from watergate_local_api.models import DeviceState +from watergate_local_api.models import DeviceState, NetworkingData, TelemetryData +from watergate_local_api.models.water_meter import WaterMeter from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_WEBHOOK_ID @@ -22,6 +23,20 @@ "battery", "1.0.0", 100, - {"volume": 1.2, "duration": 100}, + WaterMeter(1.2, 100), DEFAULT_SERIAL_NUMBER, ) + +DEFAULT_NETWORKING_STATE = NetworkingData( + True, + True, + "192.168.1.127", + "192.168.1.1", + "255.255.255.0", + "Sonic", + -50, + 2137, + 1910, +) + +DEFAULT_TELEMETRY_STATE = TelemetryData(0.0, 100, 28.32, None, []) diff --git a/tests/components/watergate/snapshots/test_sensor.ambr b/tests/components/watergate/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..a8969798105737 --- /dev/null +++ b/tests/components/watergate/snapshots/test_sensor.ambr @@ -0,0 +1,506 @@ +# serializer version: 1 +# name: test_sensor[sensor.sonic_mqtt_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_mqtt_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'MQTT up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'mqtt_up_since', + 'unique_id': 'a63182948ce2896a.mqtt_up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_mqtt_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic MQTT up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_mqtt_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:59:59+00:00', + }) +# --- +# name: test_sensor[sensor.sonic_power_supply_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'battery', + 'external', + 'battery_external', + ]), + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_power_supply_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power supply mode', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_supply_mode', + 'unique_id': 'a63182948ce2896a.power_supply_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_power_supply_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Sonic Power supply mode', + 'options': list([ + 'battery', + 'external', + 'battery_external', + ]), + }), + 'context': , + 'entity_id': 'sensor.sonic_power_supply_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'battery', + }) +# --- +# name: test_sensor[sensor.sonic_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_signal_strength', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Signal strength', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a63182948ce2896a.rssi', + 'unit_of_measurement': 'dBm', + }) +# --- +# name: test_sensor[sensor.sonic_signal_strength-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'signal_strength', + 'friendly_name': 'Sonic Signal strength', + 'state_class': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.sonic_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-50', + }) +# --- +# name: test_sensor[sensor.sonic_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'up_since', + 'unique_id': 'a63182948ce2896a.up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic Up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:58:20+00:00', + }) +# --- +# name: test_sensor[sensor.sonic_volume_flow_rate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_volume_flow_rate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Volume flow rate', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'a63182948ce2896a.water_flow_rate', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_volume_flow_rate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'volume_flow_rate', + 'friendly_name': 'Sonic Volume flow rate', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_volume_flow_rate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_meter_duration', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water meter duration', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_meter_duration', + 'unique_id': 'a63182948ce2896a.water_meter_duration', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'duration', + 'friendly_name': 'Sonic Water meter duration', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_meter_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_meter_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water meter volume', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_meter_volume', + 'unique_id': 'a63182948ce2896a.water_meter_volume', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_meter_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'water', + 'friendly_name': 'Sonic Water meter volume', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_meter_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.sonic_water_pressure-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_pressure', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water pressure', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_pressure', + 'unique_id': 'a63182948ce2896a.water_pressure', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_pressure-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', + 'friendly_name': 'Sonic Water pressure', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_pressure', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_sensor[sensor.sonic_water_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.sonic_water_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Water temperature', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'water_temperature', + 'unique_id': 'a63182948ce2896a.water_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[sensor.sonic_water_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Sonic Water temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.sonic_water_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '28.32', + }) +# --- +# name: test_sensor[sensor.sonic_wi_fi_up_since-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.sonic_wi_fi_up_since', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wi-Fi up since', + 'platform': 'watergate', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wifi_up_since', + 'unique_id': 'a63182948ce2896a.wifi_up_since', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[sensor.sonic_wi_fi_up_since-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'timestamp', + 'friendly_name': 'Sonic Wi-Fi up since', + }), + 'context': , + 'entity_id': 'sensor.sonic_wi_fi_up_since', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2021-01-09T11:59:59+00:00', + }) +# --- diff --git a/tests/components/watergate/test_sensor.py b/tests/components/watergate/test_sensor.py new file mode 100644 index 00000000000000..58632c7548bc72 --- /dev/null +++ b/tests/components/watergate/test_sensor.py @@ -0,0 +1,150 @@ +"""Tests for the Watergate valve platform.""" + +from collections.abc import Generator + +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import EntityCategory, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import init_integration +from .const import DEFAULT_NETWORKING_STATE, DEFAULT_TELEMETRY_STATE, MOCK_WEBHOOK_ID + +from tests.common import AsyncMock, MockConfigEntry, patch, snapshot_platform +from tests.typing import ClientSessionGenerator + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_sensor( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], + freezer: FrozenDateTimeFactory, + snapshot: SnapshotAssertion, +) -> None: + """Test states of the sensor.""" + freezer.move_to("2021-01-09 12:00:00+00:00") + with patch("homeassistant.components.watergate.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +async def test_diagnostics_are_disabled_by_default( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if all diagnostic entities are disabled by default.""" + with patch("homeassistant.components.watergate.PLATFORMS", [Platform.SENSOR]): + await init_integration(hass, mock_entry) + + entries = [ + entry + for entry in entity_registry.entities.get_entries_for_config_entry_id( + mock_entry.entry_id + ) + if entry.entity_category == EntityCategory.DIAGNOSTIC + ] + + assert len(entries) == 5 + for entry in entries: + assert entry.disabled + + +async def test_telemetry_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + + def assert_state(entity_id: str, expected_state: str): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + assert_state("sensor.sonic_volume_flow_rate", DEFAULT_TELEMETRY_STATE.flow) + assert_state("sensor.sonic_water_pressure", DEFAULT_TELEMETRY_STATE.pressure) + assert_state( + "sensor.sonic_water_temperature", DEFAULT_TELEMETRY_STATE.water_temperature + ) + + telemetry_change_data = { + "type": "telemetry", + "data": {"flow": 2137, "pressure": 1910, "temperature": 20}, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=telemetry_change_data) + + await hass.async_block_till_done() + + assert_state("sensor.sonic_volume_flow_rate", "2.137") + assert_state("sensor.sonic_water_pressure", "1910") + assert_state("sensor.sonic_water_temperature", "20") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_wifi_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + + def assert_state(entity_id: str, expected_state: str): + state = hass.states.get(entity_id) + assert state.state == str(expected_state) + + assert_state("sensor.sonic_signal_strength", DEFAULT_NETWORKING_STATE.rssi) + + wifi_change_data = { + "type": "wifi-changed", + "data": { + "ip": "192.168.2.137", + "gateway": "192.168.2.1", + "ssid": "Sonic 2", + "rssi": -70, + "subnet": "255.255.255.0", + }, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=wifi_change_data) + + await hass.async_block_till_done() + + assert_state("sensor.sonic_signal_strength", "-70") + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_power_supply_webhook( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + mock_entry: MockConfigEntry, + mock_watergate_client: Generator[AsyncMock], +) -> None: + """Test if water flow webhook is handled correctly.""" + await init_integration(hass, mock_entry) + entity_id = "sensor.sonic_power_supply_mode" + registered_entity = hass.states.get(entity_id) + assert registered_entity + assert registered_entity.state == "battery" + + power_supply_change_data = { + "type": "power-supply-changed", + "data": {"supply": "external"}, + } + client = await hass_client_no_auth() + await client.post(f"/api/webhook/{MOCK_WEBHOOK_ID}", json=power_supply_change_data) + + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "external"