-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
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
ReadYourMeter Pro integration #85986
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coordinators should go into their own modules, in this case, move it into |
||
"""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 |
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], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can't change, right? Why is it updated? As a matter of fact, what in this re-auth process guards that the right account is re-authed? |
||
) | ||
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for the Read Your Meter Pro integration.""" | ||
|
||
DOMAIN = "rympro" |
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" | ||
} |
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() | ||
] | ||
Comment on lines
+29
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
) | ||
|
||
|
||
class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity): | ||
"""Sensor for RymPro meters.""" | ||
|
||
_attr_has_entity_name = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be incorrect, as there are conditions not met for this to be set. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will change the name to sentence case. Other than that, is there any other condition that's not met? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's about it as it seems. |
||
_attr_name = "Last Read" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This must be following sentence case styling to be able to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Additionally, it is a bit of an odd name, it would suggest a date of when it was last read? But I guess it is water usage? Would that be a better name? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The read here is in present tense, as in the meter read. Common usage, and how the service refers to it, but maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't describe entities on what they do.. we describe what they are/represent. They represent consumption or usage in this case. |
||
_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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is unused? |
||
unique_id = f"{entry_id}_{meter_id}" | ||
frenck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self._attr_unique_id = f"{unique_id}_last_read" | ||
self._attr_extra_state_attributes = {"meter_id": str(meter_id)} | ||
frenck marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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() | ||
Comment on lines
+66
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to handle it like this. The coordinator already has the data and there are no calculations involved here. Implement the |
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%]" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -357,6 +357,7 @@ | |
"ruckus_unleashed", | ||
"ruuvi_gateway", | ||
"ruuvitag_ble", | ||
"rympro", | ||
"sabnzbd", | ||
"samsungtv", | ||
"scrape", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Tests for the Read Your Meter Pro integration.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please only wrap the line that can raise in the try... except block. Move this line down.