Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sensors platform to Watergate integration #133015

Merged
Merged
Show file tree
Hide file tree
Changes from 8 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
32 changes: 28 additions & 4 deletions homeassistant/components/watergate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]

Expand Down Expand Up @@ -82,7 +87,6 @@
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
Expand All @@ -96,9 +100,29 @@

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

Check warning on line 104 in homeassistant/components/watergate/__init__.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/watergate/__init__.py#L104

Added line #L104 was not covered by tests
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)

Expand Down
38 changes: 33 additions & 5 deletions homeassistant/components/watergate/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand All @@ -14,7 +14,22 @@
_LOGGER = logging.getLogger(__name__)


class WatergateDataCoordinator(DataUpdateCoordinator[DeviceState]):
class WatergateAgregatedRequests:
"""Class to hold aggregated requests."""

def __init__(
self,
state: DeviceState,
telemetry: TelemetryData,
networking: NetworkingData,
) -> None:
"""Initialize aggregated requests."""
self.state = state
self.telemetry = telemetry
self.networking = networking
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved


class WatergateDataCoordinator(DataUpdateCoordinator[WatergateAgregatedRequests]):
"""Class to manage fetching watergate data."""

def __init__(self, hass: HomeAssistant, api: WatergateLocalApiClient) -> None:
Expand All @@ -27,9 +42,22 @@
)
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()
return WatergateAgregatedRequests(state, telemetry, networking)
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
except WatergateApiException as exc:
raise UpdateFailed from exc
return state
raise UpdateFailed(f"Sonic device is unavailable: {exc}") from exc

Check warning on line 52 in homeassistant/components/watergate/coordinator.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/watergate/coordinator.py#L52

Added line #L52 was not covered by tests

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()
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 6 additions & 4 deletions homeassistant/components/watergate/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
)
1 change: 1 addition & 0 deletions homeassistant/components/watergate/quality_scale.yaml
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
211 changes: 211 additions & 0 deletions homeassistant/components/watergate/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
"""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.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"
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved


@dataclass(kw_only=True, frozen=True)
class WatergateSensorEntityDescription(SensorEntityDescription):
"""Description for Watergate sensor entities."""

value_fn: Callable[
[WatergateAgregatedRequests],
str | int | float | datetime | PowerSupplyMode | None,
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
]


DESCRIPTIONS: list[WatergateSensorEntityDescription] = [
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
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_uptime",
key="wifi_uptime",
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_uptime",
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
key="mqtt_uptime",
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
),
translation_key="water_flow_rate",
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
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="uptime",
key="uptime",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TIMESTAMP,
),
WatergateSensorEntityDescription(
value_fn=lambda data: PowerSupplyMode(data.state.power_supply)
if data.state
else None,
translation_key="power_supply",
key="power_supply",
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.ENUM,
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
),
]


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
)
Comment on lines +206 to +209
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't data be None?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is None it means that there was an issue with fetching those data.


@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)
31 changes: 31 additions & 0 deletions homeassistant/components/watergate/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,36 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"sensor": {
"water_meter_volume": {
"name": "Water Meter Volume"
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
},
"water_meter_duration": {
"name": "Water meter duration"
},
"wifi_uptime": {
"name": "Wi-Fi uptime"
},
"mqtt_uptime": {
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
"name": "MQTT uptime"
},
"water_temperature": {
"name": "Water temperature"
},
"water_pressure": {
"name": "Water pressure"
},
"water_flow_rate": {
"name": "Water flow rate"
},
"uptime": {
"name": "Uptime"
},
"power_supply": {
"name": "Power supply"
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
Loading