-
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
632 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
"""The Sonic Water Shut-off Valve integration.""" | ||
import logging | ||
import asyncio | ||
|
||
from herolabsapi import ( | ||
InvalidCredentialsError, | ||
Client, | ||
ServiceUnavailableError, | ||
TooManyRequestsError, | ||
) | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import CLIENT, DOMAIN | ||
from .device import SonicDeviceDataUpdateCoordinator | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
PLATFORMS: list[str] = ["switch", "sensor"] | ||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Sonic Water Shut-off Valve from a config entry.""" | ||
session = async_get_clientsession(hass) | ||
hass.data.setdefault(DOMAIN, {}) | ||
hass.data[DOMAIN][entry.entry_id] = {} | ||
try: | ||
hass.data[DOMAIN][entry.entry_id][CLIENT] = client = await Client.async_login( | ||
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session | ||
) | ||
except InvalidCredentialsError as err: | ||
raise ConfigEntryNotReady from err | ||
|
||
sonic_data = await client.sonic.async_get_all_sonic_details() | ||
|
||
_LOGGER.debug("Sonic device data information: %s", sonic_data) | ||
|
||
hass.data[DOMAIN][entry.entry_id]["devices"] = devices = [ | ||
SonicDeviceDataUpdateCoordinator(hass, client, device["id"]) | ||
for device in sonic_data["data"] | ||
] | ||
|
||
tasks = [device.async_refresh() for device in devices] | ||
await asyncio.gather(*tasks) | ||
|
||
hass.config_entries.async_setup_platforms(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
if unload_ok: | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
"""Config flow for Sonic Integration.""" | ||
from herolabsapi import Client, InvalidCredentialsError | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries, core, exceptions | ||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DOMAIN, LOGGER | ||
|
||
DATA_SCHEMA = vol.Schema({vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}) | ||
|
||
|
||
async def validate_input(hass: core.HomeAssistant, data): | ||
"""Validate the user input allows us to connect. | ||
Data has the keys from DATA_SCHEMA with values provided by the user. | ||
""" | ||
|
||
session = async_get_clientsession(hass) | ||
try: | ||
api = await Client.async_login( | ||
data[CONF_USERNAME], data[CONF_PASSWORD], session=session | ||
) | ||
except InvalidCredentialsError as request_error: | ||
LOGGER.error("Error connecting to the Sonic API: %s", request_error) | ||
raise CannotConnect from request_error | ||
|
||
# Use the verified session to discover the first sonic device's name | ||
sonic_data = await api.sonic.async_get_all_sonic_details() | ||
first_sonic_id = sonic_data["data"][0]["id"] | ||
sonic_info = await api.sonic.async_get_sonic_details(first_sonic_id) | ||
return {"title": sonic_info["name"]} | ||
|
||
|
||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Sonic.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user(self, user_input=None): | ||
"""Handle the initial step.""" | ||
errors = {} | ||
if user_input is not None: | ||
try: | ||
info = await validate_input(self.hass, user_input) | ||
|
||
return self.async_create_entry(title=info["title"], data=user_input) | ||
except CannotConnect: | ||
errors["base"] = "cannot_connect" | ||
except InvalidHost: | ||
errors["host"] = "cannot_connect" | ||
except Exception: # pylint: disable=broad-except | ||
LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=DATA_SCHEMA, errors=errors | ||
) | ||
|
||
|
||
class CannotConnect(exceptions.HomeAssistantError): | ||
"""Error to indicate we cannot connect.""" | ||
|
||
|
||
class InvalidHost(exceptions.HomeAssistantError): | ||
"""Error to indicate there is an invalid hostname.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""Constants for the Sonic Water Shut-off Valve integration.""" | ||
import logging | ||
|
||
LOGGER = logging.getLogger(__package__) | ||
|
||
CLIENT = "client" | ||
DOMAIN = "sonic" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
"""Sonic device object.""" | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
from datetime import timedelta | ||
from typing import Any | ||
|
||
from async_timeout import timeout | ||
from herolabsapi.client import Client | ||
from herolabsapi.errors import RequestError | ||
|
||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN as SONIC_DOMAIN, LOGGER | ||
|
||
|
||
class SonicDeviceDataUpdateCoordinator(DataUpdateCoordinator): | ||
"""Sonic device object.""" | ||
|
||
def __init__(self, hass: HomeAssistant, api_client: Client, device_id: str) -> None: | ||
"""Initialize the device.""" | ||
self.hass: HomeAssistant = hass | ||
self.api_client: Client = api_client | ||
self._sonic_device_id: str = device_id | ||
self._device_information: dict[str, Any] = {} | ||
self._telemetry_information: dict[str, Any] = {} | ||
super().__init__( | ||
hass, | ||
LOGGER, | ||
name=f"{SONIC_DOMAIN}-{device_id}", | ||
update_interval=timedelta(seconds=60), | ||
) | ||
|
||
async def _async_update_data(self): | ||
"""Update data via library.""" | ||
try: | ||
async with timeout(10): | ||
await asyncio.gather( | ||
*[ | ||
self._update_device(), | ||
] | ||
) | ||
except (RequestError) as error: | ||
raise UpdateFailed(error) from error | ||
|
||
@property | ||
def id(self) -> str: | ||
"""Return Sonic device id.""" | ||
return self._sonic_device_id | ||
|
||
@property | ||
def device_name(self) -> str: | ||
"""Return device name.""" | ||
return self._device_information.get("name", f"{self.model}") | ||
|
||
@property | ||
def manufacturer(self) -> str: | ||
"""Return manufacturer for device.""" | ||
return "Hero Labs" | ||
|
||
@property | ||
def serial_number(self) -> str: | ||
"""Return serial number for device.""" | ||
return self._device_information["serial_no"] | ||
|
||
@property | ||
def model(self) -> str: | ||
"""Return model for device.""" | ||
return "Sonic" | ||
|
||
@property | ||
def rssi(self) -> float: | ||
"""Return rssi for device.""" | ||
return self._device_information["radio_rssi"] | ||
|
||
@property | ||
def last_heard_from_time(self) -> str: | ||
"""Return Unix timestamp in seconds when the sonic took measurements | ||
Will need to do conversion from timestamp to datetime if HomeAssistant doesn't do it automatically""" | ||
return self._telemetry_information["probed_at"] | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return True if device is available.""" | ||
return ( | ||
self.last_update_success | ||
and self._device_information["radio_connection"] == "connected" | ||
) | ||
|
||
@property | ||
def current_flow_rate(self) -> float: | ||
"""Return current flow rate in ml/min.""" | ||
return self._telemetry_information["water_flow"] | ||
|
||
@property | ||
def current_mbar(self) -> int: | ||
"""Return the current pressure in mbar.""" | ||
return self._telemetry_information["pressure"] | ||
|
||
@property | ||
def temperature(self) -> float: | ||
"""Return the current temperature in degrees C.""" | ||
return self._telemetry_information["water_temp"] | ||
|
||
@property | ||
def battery_state(self) -> str: | ||
"""Return the battery level "high","mid","low" or returns "external_power_supply" """ | ||
return self._device_information["battery"] | ||
|
||
@property | ||
def auto_shut_off_enabled(self) -> bool: | ||
"""Return the auto shut off enabled boolean""" | ||
return self._device_information["auto_shut_off_enabled"] | ||
|
||
@property | ||
def auto_shut_off_time_limit(self) -> int: | ||
"""Return the Sonic offline auto shut off water usage time limit in seconds[0;integer::max). | ||
When set to 0 usage time check is not performed.""" | ||
return self._device_information["auto_shut_off_time_limit"] | ||
|
||
@property | ||
def auto_shut_off_volume_limit(self) -> int: | ||
"""Return the Sonic offline auto shut off used water volume limit in millilitres [0;integer::max). | ||
When set to 0 volume used check is not performed.""" | ||
return self._device_information["auto_shut_off_volume_limit"] | ||
|
||
@property | ||
def signal_id(self) -> str: | ||
"""Return the associated signal device id | ||
A Signal device (sometimes called hub) communicates with WiFi and the Sonic device""" | ||
return self._device_information["signal_id"] | ||
|
||
@property | ||
def sonic_status(self) -> str: | ||
"""Return the any sonic status message""" | ||
return self._device_information["status"] | ||
|
||
@property | ||
def last_known_valve_state(self) -> str: | ||
"""Return the current valve state | ||
Options are: 'open, closed, opening, closing, faulty, pressure_test, requested_open, requested_closed'""" | ||
return self._device_information["valve_state"] | ||
|
||
async def _update_device(self, *_) -> None: | ||
"""Update the device information from the API.""" | ||
self._device_information = await self.api_client.sonic.async_get_sonic_details( | ||
self._sonic_device_id | ||
) | ||
self._telemetry_information = ( | ||
await self.api_client.sonic.async_sonic_telemetry_by_id( | ||
self._sonic_device_id | ||
) | ||
) | ||
LOGGER.debug("Sonic device data: %s", self._device_information) | ||
LOGGER.debug("Sonic telemetry data: %s", self._telemetry_information) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
"""Base entity class for Sonic entities.""" | ||
from __future__ import annotations | ||
|
||
from typing import Any | ||
|
||
from homeassistant.helpers.entity import DeviceInfo, Entity | ||
|
||
from .const import DOMAIN as SONIC_DOMAIN | ||
from .device import SonicDeviceDataUpdateCoordinator | ||
|
||
|
||
class SonicEntity(Entity): | ||
"""A base class for Sonic entities.""" | ||
|
||
_attr_force_update = False | ||
_attr_should_poll = False | ||
|
||
def __init__( | ||
self, | ||
entity_type: str, | ||
name: str, | ||
device: SonicDeviceDataUpdateCoordinator, | ||
**kwargs, | ||
) -> None: | ||
"""Init Sonic entity.""" | ||
self._attr_name = name | ||
self._attr_unique_id = f"{device.serial_number}_{entity_type}" | ||
|
||
self._device: SonicDeviceDataUpdateCoordinator = device | ||
self._state: Any = None | ||
|
||
@property | ||
def device_info(self) -> DeviceInfo: | ||
"""Return a device description for device registry.""" | ||
return DeviceInfo( | ||
identifiers={(SONIC_DOMAIN, self._device.id)}, | ||
manufacturer=self._device.manufacturer, | ||
model=self._device.model, | ||
name=self._device.device_name, | ||
) | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return True if device is available.""" | ||
return self._device.available | ||
|
||
async def async_update(self): | ||
"""Update Sonic entity.""" | ||
await self._device.async_request_refresh() | ||
|
||
async def async_added_to_hass(self): | ||
"""When entity is added to hass.""" | ||
self.async_on_remove(self._device.async_add_listener(self.async_write_ha_state)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "sonic", | ||
"name": "Sonic by Hero Labs", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/sonic", | ||
"requirements": ["herolabsapi==0.3.6"], | ||
"codeowners": ["@markvader"], | ||
"iot_class": "cloud_polling", | ||
"loggers": ["sonic"] | ||
} |
Oops, something went wrong.