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

ReadYourMeter Pro integration #85986

Merged
merged 4 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,8 @@ omit =
homeassistant/components/ruuvi_gateway/coordinator.py
homeassistant/components/russound_rio/media_player.py
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/rympro/__init__.py
homeassistant/components/rympro/sensor.py
homeassistant/components/sabnzbd/__init__.py
homeassistant/components/sabnzbd/sensor.py
homeassistant/components/saj/sensor.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,8 @@ build.json @home-assistant/supervisor
/tests/components/ruuvi_gateway/ @akx
/homeassistant/components/ruuvitag_ble/ @akx
/tests/components/ruuvitag_ble/ @akx
/homeassistant/components/rympro/ @OnFreund
/tests/components/rympro/ @OnFreund
/homeassistant/components/sabnzbd/ @shaiu
/tests/components/sabnzbd/ @shaiu
/homeassistant/components/safe_mode/ @home-assistant/core
Expand Down
79 changes: 79 additions & 0 deletions homeassistant/components/rympro/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""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

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(
Copy link
Member

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.

entry,
data={**data, CONF_TOKEN: token},
)
except UnauthorizedError as error:
raise ConfigEntryAuthFailed from error

coordinator = RymProDataUpdateCoordinator(hass, rympro, 60 * 60)
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
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):
Copy link
Member

Choose a reason for hiding this comment

The 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 coordinator.py.

"""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 (CannotConnectError, UnauthorizedError, OperationError) as error:
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
raise UpdateFailed(error) from error
98 changes: 98 additions & 0 deletions homeassistant/components/rympro/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""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],
Copy link
Member

@frenck frenck Jan 24, 2023

Choose a reason for hiding this comment

The 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 = await self.async_set_unique_id(entry_data[CONF_UNIQUE_ID])
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
return await self.async_step_user()
3 changes: 3 additions & 0 deletions homeassistant/components/rympro/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the Read Your Meter Pro integration."""

DOMAIN = "rympro"
13 changes: 13 additions & 0 deletions homeassistant/components/rympro/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "rympro",
"name": "Read Your Meter Pro",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rympro",
"requirements": ["pyrympro==0.0.4"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
"codeowners": ["@OnFreund"],
"iot_class": "cloud_polling"
}
69 changes: 69 additions & 0 deletions homeassistant/components/rympro/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""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
Copy link
Member

Choose a reason for hiding this comment

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

async_add_entities accepts a generator, you can remove the list comprehension here.

)


class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity):
"""Sensor for RymPro meters."""

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
Copy link
Member

Choose a reason for hiding this comment

The 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_has_entity_name = True
self._attr_name = "Last Read"
self._attr_device_class = SensorDeviceClass.WATER
self._attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The 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 native_value property method instead.

20 changes: 20 additions & 0 deletions homeassistant/components/rympro/strings.json
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%]"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@
"ruckus_unleashed",
"ruuvi_gateway",
"ruuvitag_ble",
"rympro",
"sabnzbd",
"samsungtv",
"scrape",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -4620,6 +4620,12 @@
"config_flow": true,
"iot_class": "local_push"
},
"rympro": {
"name": "Read Your Meter Pro",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"sabnzbd": {
"name": "SABnzbd",
"integration_type": "hub",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1904,6 +1904,9 @@ pyrituals==0.0.6
# homeassistant.components.ruckus_unleashed
pyruckus==0.16

# homeassistant.components.rympro
pyrympro==0.0.4

# homeassistant.components.sabnzbd
pysabnzbd==1.1.1

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1369,6 +1369,9 @@ pyrituals==0.0.6
# homeassistant.components.ruckus_unleashed
pyruckus==0.16

# homeassistant.components.rympro
pyrympro==0.0.4

# homeassistant.components.sabnzbd
pysabnzbd==1.1.1

Expand Down
1 change: 1 addition & 0 deletions tests/components/rympro/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Read Your Meter Pro integration."""
Loading