Skip to content

Commit

Permalink
Rympro integration code fixes (#86734)
Browse files Browse the repository at this point in the history
* Address review comments

* Add coordinator.py to coveragerc

* Apply suggestions from code review

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>

* Update homeassistant/components/rympro/coordinator.py

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>

* Move SCAN_INTERVAL to coordinator.py

---------

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  • Loading branch information
OnFreund and cdce8p authored Jan 31, 2023
1 parent a28e7e1 commit 35b82db
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 51 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ omit =
homeassistant/components/russound_rio/media_player.py
homeassistant/components/russound_rnet/media_player.py
homeassistant/components/rympro/__init__.py
homeassistant/components/rympro/coordinator.py
homeassistant/components/rympro/sensor.py
homeassistant/components/sabnzbd/__init__.py
homeassistant/components/sabnzbd/sensor.py
Expand Down
4 changes: 2 additions & 2 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -1003,8 +1003,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/rympro/ @OnFreund @elad-bar @maorcc
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
/homeassistant/components/sabnzbd/ @shaiu
/tests/components/sabnzbd/ @shaiu
/homeassistant/components/safe_mode/ @home-assistant/core
Expand Down
40 changes: 7 additions & 33 deletions homeassistant/components/rympro/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
"""The Read Your Meter Pro integration."""
from __future__ import annotations

from datetime import timedelta
import logging

from pyrympro import CannotConnectError, OperationError, RymPro, UnauthorizedError
from pyrympro import CannotConnectError, 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
from .coordinator import RymProDataUpdateCoordinator

SCAN_INTERVAL = 60 * 60
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)

Expand All @@ -32,14 +30,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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
hass.config_entries.async_update_entry(
entry,
data={**data, CONF_TOKEN: token},
)

coordinator = RymProDataUpdateCoordinator(hass, rympro, SCAN_INTERVAL)
coordinator = RymProDataUpdateCoordinator(hass, rympro)
await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})
Expand All @@ -56,27 +54,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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
42 changes: 42 additions & 0 deletions homeassistant/components/rympro/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""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.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN

SCAN_INTERVAL = 60 * 60

_LOGGER = logging.getLogger(__name__)


class RymProDataUpdateCoordinator(DataUpdateCoordinator[dict[int, dict]]):
"""Class to manage fetching RYM Pro data."""

def __init__(self, hass: HomeAssistant, rympro: RymPro) -> 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) -> dict[int, dict]:
"""Fetch data from Rym Pro."""
try:
return await self.rympro.last_read()
except UnauthorizedError as error:
assert self.config_entry
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
raise UpdateFailed(error) from error
except (CannotConnectError, OperationError) as error:
raise UpdateFailed(error) from error
2 changes: 1 addition & 1 deletion homeassistant/components/rympro/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rympro",
"requirements": ["pyrympro==0.0.4"],
"codeowners": ["@OnFreund"],
"codeowners": ["@OnFreund", "@elad-bar", "@maorcc"],
"iot_class": "cloud_polling"
}
25 changes: 10 additions & 15 deletions homeassistant/components/rympro/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
)
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.core import HomeAssistant
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
from .coordinator import RymProDataUpdateCoordinator


async def async_setup_entry(
Expand All @@ -26,18 +25,16 @@ async def async_setup_entry(
"""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()
]
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_name = "Total consumption"
_attr_device_class = SensorDeviceClass.WATER
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
_attr_state_class = SensorStateClass.TOTAL_INCREASING
Expand All @@ -52,9 +49,8 @@ def __init__(
"""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_unique_id = f"{unique_id}_total_consumption"
self._attr_extra_state_attributes = {"meter_id": str(meter_id)}
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
Expand All @@ -63,8 +59,7 @@ def __init__(
)
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()
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
return self.coordinator.data[self._meter_id]["read"]
24 changes: 24 additions & 0 deletions tests/components/rympro/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ async def test_login_error(hass, exception, error):
assert result2["type"] == FlowResultType.FORM
assert result2["errors"] == {"base": error}

with patch(
"homeassistant.components.rympro.config_flow.RymPro.login",
return_value="test-token",
), patch(
"homeassistant.components.rympro.config_flow.RymPro.account_info",
return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]},
), patch(
"homeassistant.components.rympro.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_EMAIL: TEST_DATA[CONF_EMAIL],
CONF_PASSWORD: TEST_DATA[CONF_PASSWORD],
},
)
await hass.async_block_till_done()

assert result3["type"] == FlowResultType.CREATE_ENTRY
assert result3["title"] == TEST_DATA[CONF_EMAIL]
assert result3["data"] == TEST_DATA
assert len(mock_setup_entry.mock_calls) == 1


async def test_form_already_exists(hass, _config_entry):
"""Test that a flow with an existing account aborts."""
Expand Down

0 comments on commit 35b82db

Please sign in to comment.