Skip to content

Commit

Permalink
Add PECO smart meter binary_sensor (#71034)
Browse files Browse the repository at this point in the history
* Add support for PECO smart meter

* Add support for PECO smart meter

* Conform to black

* Fix tests and additional clean-up

* Return init file to original state

* Move to FlowResultType

* Catch up to upstream

* Remove commented code

* isort

* Merge smart meter and outage count into one entry

* Test coverage

* Remove logging exceptions from config flow verification

* Fix comments from @emontnemery

* Revert "Add support for PECO smart meter"

This reverts commit 36ca908.

* More fixes
  • Loading branch information
IceBotYT authored Nov 29, 2023
1 parent 3aa9066 commit 526180a
Show file tree
Hide file tree
Showing 8 changed files with 516 additions and 34 deletions.
58 changes: 50 additions & 8 deletions homeassistant/components/peco/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,31 @@
from datetime import timedelta
from typing import Final

from peco import AlertResults, BadJSONError, HttpError, OutageResults, PecoOutageApi
from peco import (
AlertResults,
BadJSONError,
HttpError,
OutageResults,
PecoOutageApi,
UnresponsiveMeterError,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_COUNTY, DOMAIN, LOGGER, SCAN_INTERVAL
from .const import (
CONF_COUNTY,
CONF_PHONE_NUMBER,
DOMAIN,
LOGGER,
OUTAGE_SCAN_INTERVAL,
SMART_METER_SCAN_INTERVAL,
)

PLATFORMS: Final = [Platform.SENSOR]
PLATFORMS: Final = [Platform.SENSOR, Platform.BINARY_SENSOR]


@dataclass
Expand All @@ -31,9 +45,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

websession = async_get_clientsession(hass)
api = PecoOutageApi()
# Outage Counter Setup
county: str = entry.data[CONF_COUNTY]

async def async_update_data() -> PECOCoordinatorData:
async def async_update_outage_data() -> OutageResults:
"""Fetch data from API."""
try:
outages: OutageResults = (
Expand All @@ -53,15 +68,42 @@ async def async_update_data() -> PECOCoordinatorData:
hass,
LOGGER,
name="PECO Outage Count",
update_method=async_update_data,
update_interval=timedelta(minutes=SCAN_INTERVAL),
update_method=async_update_outage_data,
update_interval=timedelta(minutes=OUTAGE_SCAN_INTERVAL),
)

await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {"outage_count": coordinator}

if phone_number := entry.data.get(CONF_PHONE_NUMBER):
# Smart Meter Setup]

async def async_update_meter_data() -> bool:
"""Fetch data from API."""
try:
data: bool = await api.meter_check(phone_number, websession)
except UnresponsiveMeterError as err:
raise UpdateFailed("Unresponsive meter") from err
except HttpError as err:
raise UpdateFailed(f"Error fetching data: {err}") from err
except BadJSONError as err:
raise UpdateFailed(f"Error parsing data: {err}") from err
return data

coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name="PECO Smart Meter",
update_method=async_update_meter_data,
update_interval=timedelta(minutes=SMART_METER_SCAN_INTERVAL),
)

await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN][entry.entry_id]["smart_meter"] = coordinator

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


Expand Down
59 changes: 59 additions & 0 deletions homeassistant/components/peco/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Binary sensor for PECO outage counter."""
from __future__ import annotations

from typing import Final

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from .const import DOMAIN

PARALLEL_UPDATES: Final = 0


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary sensor for PECO."""
if "smart_meter" not in hass.data[DOMAIN][config_entry.entry_id]:
return
coordinator: DataUpdateCoordinator[bool] = hass.data[DOMAIN][config_entry.entry_id][
"smart_meter"
]

async_add_entities(
[PecoBinarySensor(coordinator, phone_number=config_entry.data["phone_number"])]
)


class PecoBinarySensor(
CoordinatorEntity[DataUpdateCoordinator[bool]], BinarySensorEntity
):
"""Binary sensor for PECO outage counter."""

_attr_icon = "mdi:gauge"
_attr_device_class = BinarySensorDeviceClass.POWER
_attr_name = "Meter Status"

def __init__(
self, coordinator: DataUpdateCoordinator[bool], phone_number: str
) -> None:
"""Initialize binary sensor for PECO."""
super().__init__(coordinator)
self._attr_unique_id = f"{phone_number}"

@property
def is_on(self) -> bool:
"""Return if the meter has power."""
return self.coordinator.data
89 changes: 85 additions & 4 deletions homeassistant/components/peco/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,122 @@
"""Config flow for PECO Outage Counter integration."""
from __future__ import annotations

import logging
from typing import Any

from peco import (
HttpError,
IncompatibleMeterError,
PecoOutageApi,
UnresponsiveMeterError,
)
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv

from .const import CONF_COUNTY, COUNTY_LIST, DOMAIN
from .const import CONF_COUNTY, CONF_PHONE_NUMBER, COUNTY_LIST, DOMAIN

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_COUNTY): vol.In(COUNTY_LIST),
vol.Optional(CONF_PHONE_NUMBER): cv.string,
}
)

_LOGGER = logging.getLogger(__name__)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for PECO Outage Counter."""

VERSION = 1

meter_verification: bool = False
meter_data: dict[str, str] = {}
meter_error: dict[str, str] = {}

async def _verify_meter(self, phone_number: str) -> None:
"""Verify if the meter is compatible."""

api = PecoOutageApi()

try:
await api.meter_check(phone_number)
except ValueError:
self.meter_error = {"phone_number": "invalid_phone_number", "type": "error"}
except IncompatibleMeterError:
self.meter_error = {"phone_number": "incompatible_meter", "type": "abort"}
except UnresponsiveMeterError:
self.meter_error = {"phone_number": "unresponsive_meter", "type": "error"}
except HttpError:
self.meter_error = {"phone_number": "http_error", "type": "error"}

self.hass.async_create_task(
self.hass.config_entries.flow.async_configure(flow_id=self.flow_id)
)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if self.meter_verification is True:
return self.async_show_progress_done(next_step_id="finish_smart_meter")

if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
)

county = user_input[CONF_COUNTY]

await self.async_set_unique_id(county)
if CONF_PHONE_NUMBER not in user_input:
await self.async_set_unique_id(county)
self._abort_if_unique_id_configured()

return self.async_create_entry(
title=f"{user_input[CONF_COUNTY].capitalize()} Outage Count",
data=user_input,
)

phone_number = user_input[CONF_PHONE_NUMBER]

await self.async_set_unique_id(f"{county}-{phone_number}")
self._abort_if_unique_id_configured()

self.meter_verification = True

if self.meter_error is not None:
# Clear any previous errors, since the user may have corrected them
self.meter_error = {}

self.hass.async_create_task(self._verify_meter(phone_number))

self.meter_data = user_input

return self.async_show_progress(
step_id="user",
progress_action="verifying_meter",
)

async def async_step_finish_smart_meter(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the finish smart meter step."""
if "phone_number" in self.meter_error:
if self.meter_error["type"] == "error":
self.meter_verification = False
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors={"phone_number": self.meter_error["phone_number"]},
)

return self.async_abort(reason=self.meter_error["phone_number"])

return self.async_create_entry(
title=f"{county.capitalize()} Outage Count", data=user_input
title=f"{self.meter_data[CONF_COUNTY].capitalize()} - {self.meter_data[CONF_PHONE_NUMBER]}",
data=self.meter_data,
)
4 changes: 3 additions & 1 deletion homeassistant/components/peco/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"TOTAL",
]
CONFIG_FLOW_COUNTIES: Final = [{county: county.capitalize()} for county in COUNTY_LIST]
SCAN_INTERVAL: Final = 9
OUTAGE_SCAN_INTERVAL: Final = 9 # minutes
SMART_METER_SCAN_INTERVAL: Final = 15 # minutes
CONF_COUNTY: Final = "county"
ATTR_CONTENT: Final = "content"
CONF_PHONE_NUMBER: Final = "phone_number"
2 changes: 1 addition & 1 deletion homeassistant/components/peco/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def async_setup_entry(
) -> None:
"""Set up the sensor platform."""
county: str = config_entry.data[CONF_COUNTY]
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = hass.data[DOMAIN][config_entry.entry_id]["outage_count"]

async_add_entities(
PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST
Expand Down
18 changes: 16 additions & 2 deletions homeassistant/components/peco/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@
"step": {
"user": {
"data": {
"county": "County"
"county": "County",
"phone_number": "Phone Number"
},
"data_description": {
"county": "County used for outage number retrieval",
"phone_number": "Phone number associated with the PECO account (optional). Adding a phone number adds a binary sensor confirming if your power is out or not, and not an issue with a breaker or an issue on your end."
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"incompatible_meter": "Your meter is not compatible with smart meter checking."
},
"progress": {
"verifying_meter": "One moment. Verifying that your meter is compatible. This may take a minute or two."
},
"error": {
"invalid_phone_number": "Please enter a valid phone number.",
"unresponsive_meter": "Your meter is not responding. Please try again later.",
"http_error": "There was an error communicating with PECO. The issue that is most likely is that you entered an invalid phone number. Please check the phone number or try again later."
}
},
"entity": {
Expand Down
Loading

0 comments on commit 526180a

Please sign in to comment.