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 all 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 @@ 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
Expand All @@ -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)

Expand Down
33 changes: 28 additions & 5 deletions homeassistant/components/watergate/coordinator.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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()
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
),
)
1 change: 1 addition & 0 deletions homeassistant/components/watergate/quality_scale.yaml
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
214 changes: 214 additions & 0 deletions homeassistant/components/watergate/sensor.py
Original file line number Diff line number Diff line change
@@ -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] = [
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_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,
adam-the-hero marked this conversation as resolved.
Show resolved Hide resolved
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
)
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)
33 changes: 33 additions & 0 deletions homeassistant/components/watergate/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
Loading