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

Rympro integration code fixes #86734

Merged
merged 5 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,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 @@ -1001,8 +1001,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