-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ReadYourMeter Pro integration (#85986)
* ReadYourMeter Pro integration * Add __init__.py to .coveragerc * Address code review comments * More code review comments
- Loading branch information
Showing
14 changed files
with
504 additions
and
0 deletions.
There are no files selected for viewing
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
Validating CODEOWNERS rules …
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
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,82 @@ | ||
"""The Read Your Meter Pro integration.""" | ||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
from pyrympro import CannotConnectError, OperationError, RymPro, UnauthorizedError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN | ||
|
||
SCAN_INTERVAL = 60 * 60 | ||
PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Read Your Meter Pro from a config entry.""" | ||
data = entry.data | ||
rympro = RymPro(async_get_clientsession(hass)) | ||
rympro.set_token(data[CONF_TOKEN]) | ||
try: | ||
await rympro.account_info() | ||
except CannotConnectError as error: | ||
raise ConfigEntryNotReady from error | ||
except UnauthorizedError: | ||
try: | ||
token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") | ||
hass.config_entries.async_update_entry( | ||
entry, | ||
data={**data, CONF_TOKEN: token}, | ||
) | ||
except UnauthorizedError as error: | ||
raise ConfigEntryAuthFailed from error | ||
|
||
coordinator = RymProDataUpdateCoordinator(hass, rympro, SCAN_INTERVAL) | ||
await coordinator.async_config_entry_first_refresh() | ||
|
||
hass.data.setdefault(DOMAIN, {}) | ||
hass.data[DOMAIN][entry.entry_id] = coordinator | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok | ||
|
||
|
||
class RymProDataUpdateCoordinator(DataUpdateCoordinator): | ||
"""Class to manage fetching RYM Pro data.""" | ||
|
||
def __init__(self, hass: HomeAssistant, rympro: RymPro, scan_interval: int) -> None: | ||
"""Initialize global RymPro data updater.""" | ||
self.rympro = rympro | ||
interval = timedelta(seconds=scan_interval) | ||
super().__init__( | ||
hass, | ||
_LOGGER, | ||
name=DOMAIN, | ||
update_interval=interval, | ||
) | ||
|
||
async def _async_update_data(self): | ||
"""Fetch data from Rym Pro.""" | ||
try: | ||
return await self.rympro.last_read() | ||
except UnauthorizedError: | ||
await self.hass.config_entries.async_reload(self.config_entry.entry_id) | ||
except (CannotConnectError, OperationError) as error: | ||
raise UpdateFailed(error) from error |
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,100 @@ | ||
"""Config flow for Read Your Meter Pro integration.""" | ||
from __future__ import annotations | ||
|
||
from collections.abc import Mapping | ||
import logging | ||
from typing import Any | ||
|
||
from pyrympro import CannotConnectError, RymPro, UnauthorizedError | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries | ||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.data_entry_flow import FlowResult | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_EMAIL): str, | ||
vol.Required(CONF_PASSWORD): str, | ||
} | ||
) | ||
|
||
|
||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: | ||
"""Validate the user input allows us to connect. | ||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. | ||
""" | ||
|
||
rympro = RymPro(async_get_clientsession(hass)) | ||
|
||
token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") | ||
|
||
info = await rympro.account_info() | ||
|
||
return {CONF_TOKEN: token, CONF_UNIQUE_ID: info["accountNumber"]} | ||
|
||
|
||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Read Your Meter Pro.""" | ||
|
||
VERSION = 1 | ||
|
||
def __init__(self) -> None: | ||
"""Init the config flow.""" | ||
self._reauth_entry: config_entries.ConfigEntry | None = None | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> FlowResult: | ||
"""Handle the initial step.""" | ||
if user_input is None: | ||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA | ||
) | ||
|
||
errors = {} | ||
|
||
try: | ||
info = await validate_input(self.hass, user_input) | ||
except CannotConnectError: | ||
errors["base"] = "cannot_connect" | ||
except UnauthorizedError: | ||
errors["base"] = "invalid_auth" | ||
except Exception: # pylint: disable=broad-except | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
title = user_input[CONF_EMAIL] | ||
data = {**user_input, **info} | ||
|
||
if not self._reauth_entry: | ||
await self.async_set_unique_id(info[CONF_UNIQUE_ID]) | ||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry(title=title, data=data) | ||
|
||
self.hass.config_entries.async_update_entry( | ||
self._reauth_entry, | ||
title=title, | ||
data=data, | ||
unique_id=info[CONF_UNIQUE_ID], | ||
) | ||
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) | ||
return self.async_abort(reason="reauth_successful") | ||
|
||
return self.async_show_form( | ||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors | ||
) | ||
|
||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: | ||
"""Handle configuration by re-auth.""" | ||
self._reauth_entry = self.hass.config_entries.async_get_entry( | ||
self.context["entry_id"] | ||
) | ||
return await self.async_step_user() |
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,3 @@ | ||
"""Constants for the Read Your Meter Pro integration.""" | ||
|
||
DOMAIN = "rympro" |
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,9 @@ | ||
{ | ||
"domain": "rympro", | ||
"name": "Read Your Meter Pro", | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/rympro", | ||
"requirements": ["pyrympro==0.0.4"], | ||
"codeowners": ["@OnFreund"], | ||
"iot_class": "cloud_polling" | ||
} |
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,70 @@ | ||
"""Sensor for RymPro meters.""" | ||
from __future__ import annotations | ||
|
||
from homeassistant.components.sensor import ( | ||
SensorDeviceClass, | ||
SensorEntity, | ||
SensorStateClass, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import UnitOfVolume | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers import entity_registry as er | ||
from homeassistant.helpers.entity import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from . import RymProDataUpdateCoordinator | ||
from .const import DOMAIN | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Set up sensors for device.""" | ||
coordinator: RymProDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] | ||
async_add_entities( | ||
[ | ||
RymProSensor(coordinator, meter_id, meter["read"], config_entry.entry_id) | ||
for meter_id, meter in coordinator.data.items() | ||
] | ||
) | ||
|
||
|
||
class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity): | ||
"""Sensor for RymPro meters.""" | ||
|
||
_attr_has_entity_name = True | ||
_attr_name = "Last Read" | ||
_attr_device_class = SensorDeviceClass.WATER | ||
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS | ||
_attr_state_class = SensorStateClass.TOTAL_INCREASING | ||
|
||
def __init__( | ||
self, | ||
coordinator: RymProDataUpdateCoordinator, | ||
meter_id: int, | ||
last_read: int, | ||
entry_id: str, | ||
) -> None: | ||
"""Initialize sensor.""" | ||
super().__init__(coordinator) | ||
self._meter_id = meter_id | ||
self._entity_registry: er.EntityRegistry | None = None | ||
unique_id = f"{entry_id}_{meter_id}" | ||
self._attr_unique_id = f"{unique_id}_last_read" | ||
self._attr_extra_state_attributes = {"meter_id": str(meter_id)} | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, unique_id)}, | ||
manufacturer="Read Your Meter Pro", | ||
name=f"Meter {meter_id}", | ||
) | ||
self._attr_native_value = last_read | ||
|
||
@callback | ||
def _handle_coordinator_update(self) -> None: | ||
"""Handle updated data from the coordinator.""" | ||
self._attr_native_value = self.coordinator.data[self._meter_id]["read"] | ||
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,20 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"data": { | ||
"email": "[%key:common::config_flow::data::email%]", | ||
"password": "[%key:common::config_flow::data::password%]" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]" | ||
}, | ||
"abort": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" | ||
} | ||
} | ||
} |
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 |
---|---|---|
|
@@ -359,6 +359,7 @@ | |
"ruckus_unleashed", | ||
"ruuvi_gateway", | ||
"ruuvitag_ble", | ||
"rympro", | ||
"sabnzbd", | ||
"samsungtv", | ||
"scrape", | ||
|
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
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
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
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 @@ | ||
"""Tests for the Read Your Meter Pro integration.""" |
Oops, something went wrong.