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

Add PECO power outage counter integration #65194

Merged
merged 40 commits into from
Mar 21, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
38309c5
Create a new NWS Alerts integration
IceBotYT Jan 24, 2022
aeabdd3
Create a new NWS Alerts integration
IceBotYT Jan 24, 2022
03c9141
Create new PECO integration
IceBotYT Jan 29, 2022
34d051a
Remove empty keys
IceBotYT Jan 29, 2022
906e4f8
Revert "Create a new NWS Alerts integration"
IceBotYT Jan 29, 2022
15e8d40
Revert "Create a new NWS Alerts integration"
IceBotYT Jan 29, 2022
f646cd9
Fix test with new mock data
IceBotYT Jan 29, 2022
ced625f
Add init and sensor to .coveragerc and more tests for config flow
IceBotYT Jan 29, 2022
fe96df9
Small fixes and replacing patch with pytest.raises in testing invalid…
IceBotYT Jan 30, 2022
1a3de51
Add type defs and fix test_config_flow to use MultipleValid instead
IceBotYT Jan 30, 2022
5f708a6
Fix issues with 'typing.Dict'
IceBotYT Jan 30, 2022
cfb772a
Move API communication to seperate PyPI library
IceBotYT Jan 31, 2022
d267e43
Switch PyPI library from httpx to aiohttp to allow for passing in web…
IceBotYT Feb 1, 2022
586d8ff
Commit file changes requested by farmio as listed here: https://githu…
IceBotYT Feb 2, 2022
813b1f4
Add suggestions requested by farmio as listed here: https://github.co…
IceBotYT Feb 3, 2022
5a16de2
Move native_unit_of_measurement from prop to attr
IceBotYT Feb 4, 2022
d056b01
Update PLATFORMS constant type annotation
IceBotYT Feb 4, 2022
c452e8d
Add peco to .strict-typing
IceBotYT Feb 4, 2022
ad0b648
Merge branch 'dev' into peco-outage-count
IceBotYT Feb 4, 2022
17ab75d
Forgot to import Final
IceBotYT Feb 4, 2022
387ef99
Do as requested [here](https://github.com/home-assistant/core/runs/50…
IceBotYT Feb 4, 2022
20490c0
Updated mypy.ini, checks should pass now
IceBotYT Feb 4, 2022
f82aaaa
Fix to conform to mypy restrictions https://github.com/home-assistant…
IceBotYT Feb 4, 2022
52804b2
Fix type annotations
IceBotYT Feb 4, 2022
e06a126
Fix tests
IceBotYT Feb 4, 2022
465380e
Use cast in async_update_data
IceBotYT Feb 4, 2022
5b12a18
Add data type to CoordinatorEntity and DataUpdateCoordinator
IceBotYT Feb 5, 2022
92e6bd8
Merge branch 'dev' into peco-outage-count
IceBotYT Mar 12, 2022
e40955a
More cleanup from suggestions here: https://github.com/home-assistant…
IceBotYT Mar 14, 2022
0f19d62
Fix tests for new code
IceBotYT Mar 14, 2022
cba3b90
Merge branch 'peco-outage-count' of https://github.com/IceBotYT/core …
IceBotYT Mar 14, 2022
a276553
Cleaning up a speck of dust
IceBotYT Mar 14, 2022
7265ff2
Remove unused variable from the peco sensor
IceBotYT Mar 15, 2022
1704d36
Add rounding to percentage, and extra clean-up
IceBotYT Mar 15, 2022
b83e77f
Final suggestions from @farmio
IceBotYT Mar 20, 2022
fd11fb6
Update SCAN_INTERVAL to be a little bit faster
IceBotYT Mar 20, 2022
53d2625
Change the SCAN_INTERVAL to be somewhat near the update interval of t…
IceBotYT Mar 20, 2022
9491e60
Merge remote-tracking branch 'upstream/dev' into peco-outage-count
IceBotYT Mar 20, 2022
a8e9218
Fix config_flows.py to new method
IceBotYT Mar 21, 2022
49c180b
New UpdateCoordinator typing
IceBotYT Mar 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,8 @@ tests/components/panel_custom/* @home-assistant/frontend
homeassistant/components/panel_iframe/* @home-assistant/frontend
tests/components/panel_iframe/* @home-assistant/frontend
homeassistant/components/pcal9535a/* @Shulyaka
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
homeassistant/components/peco/* @IceBotYT
tests/components/peco/* @IceBotYT
homeassistant/components/persistent_notification/* @home-assistant/core
tests/components/persistent_notification/* @home-assistant/core
homeassistant/components/philips_js/* @elupus
Expand Down
43 changes: 43 additions & 0 deletions homeassistant/components/peco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""The PECO Outage Counter integration."""
from __future__ import annotations

import peco

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import COUNTY_LIST, DOMAIN

PLATFORMS: list[str] = [Platform.SENSOR]
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up PECO Outage Counter from a config entry."""

if entry.data["county"] not in COUNTY_LIST:
raise InvalidCountyError(f"{entry.data['county']} is not a valid county")
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
"api": peco.PecoOutageApi(),
"websession": async_get_clientsession(hass),
}
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
hass.config_entries.async_setup_platforms(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

return unload_ok


class InvalidCountyError(HomeAssistantError):
"""Error to indicate an invalid county."""
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
52 changes: 52 additions & 0 deletions homeassistant/components/peco/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Config flow for PECO Outage Counter integration."""
from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.data_entry_flow import FlowResult

from .const import COUNTY_LIST, DOMAIN

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("county"): vol.In(COUNTY_LIST),
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
}
)


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

VERSION = 1

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
)

county = user_input[
"county"
] # Voluptuous automatically detects if the county is invalid
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

await self.async_set_unique_id(county)
self._abort_if_unique_id_configured() # this should abort if the county was already configured

# take the county name and make it all lowercase except for the first letter
# this is to make it easier to read in the UI
county_name = county.lower()
county_name = county_name[0].upper() + county_name[1:]
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

return self.async_create_entry(
title=county_name + " Outage Count", data=user_input
)

async def async_step_reauth(self, user_input: dict[str, Any] = None) -> FlowResult:
"""Handle re-auth step."""
return await self.async_step_user(user_input)
29 changes: 29 additions & 0 deletions homeassistant/components/peco/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Constants for the PECO Outage Counter integration."""
import logging

from homeassistant.components.sensor import SensorEntityDescription

DOMAIN = "peco"
_LOGGER = logging.getLogger(__name__)
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
COUNTY_LIST = [
"BUCKS",
"CHESTER",
"DELAWARE",
"MONTGOMERY",
"PHILADELPHIA",
"YORK",
"TOTAL",
]
SCAN_INTERVAL = 5
ATTR_CUSTOMERS_OUT = "customers_out"
ATTR_PERCENT_CUSTOMERS_OUT = "percent_customers_out"
ATTR_OUTAGE_COUNT = "outage_count"
ATTR_CUSTOMERS_SERVED = "customers_served"
SENSOR_LIST = (
SensorEntityDescription(key=ATTR_CUSTOMERS_OUT, name="{} Customers Out"),
SensorEntityDescription(
key=ATTR_PERCENT_CUSTOMERS_OUT, name="{} Percent Customers Out"
),
SensorEntityDescription(key=ATTR_OUTAGE_COUNT, name="{} Outage Count"),
SensorEntityDescription(key=ATTR_CUSTOMERS_SERVED, name="{} Customers Served"),
)
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
13 changes: 13 additions & 0 deletions homeassistant/components/peco/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "peco",
"name": "PECO Outage Counter",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/peco",
"codeowners": [
"@IceBotYT"
],
"iot_class": "cloud_polling",
"requirements": [
"peco==0.0.19"
]
}
109 changes: 109 additions & 0 deletions homeassistant/components/peco/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Sensor component for PECO outage counter."""
from datetime import timedelta
from types import MappingProxyType
from typing import Any, Final

import async_timeout
from peco import BadJSONError, HttpError

from homeassistant.components.sensor import SensorEntity, SensorStateClass
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,
UpdateFailed,
)

from .const import _LOGGER, DOMAIN, SCAN_INTERVAL, SENSOR_LIST

PARALLEL_UPDATES: Final = 0
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
api = hass.data[DOMAIN][config_entry.entry_id]["api"]
websession = hass.data[DOMAIN][config_entry.entry_id]["websession"]
conf: MappingProxyType[str, Any] = config_entry.data
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

async def async_update_data():
"""Fetch data from API."""
async with async_timeout.timeout(10):
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
if conf["county"] == "TOTAL":
try:
data = await api.get_outage_totals(websession)
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
if data["percent_customers_out"] < 5:
data["percent_customers_out"] = "Less than 5%"
return data
try:
data = await api.get_outage_count(conf["county"], websession)
except HttpError as err:
raise UpdateFailed from err
except BadJSONError as err:
raise UpdateFailed from err
if data["percent_customers_out"] < 5:
data["percent_customers_out"] = "Less than 5%"
return data

coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="PECO Outage Count",
update_method=async_update_data,
update_interval=timedelta(minutes=SCAN_INTERVAL),
)

await coordinator.async_config_entry_first_refresh()
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

county = conf["county"]

county_name = county.lower()[0].upper() + county.lower()[1:]
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved

sensors = []
for sensor in SENSOR_LIST:
sensor_name = getattr(sensor, "name", "This should never happen").format(
county_name
)
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
sensors.append(PecoSensor(hass, sensor_name, county, coordinator, sensor.key))
async_add_entities(
sensors,
True,
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
)
return
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved


class PecoSensor(CoordinatorEntity, SensorEntity):
"""PECO outage counter sensor."""

_attr_state_class = SensorStateClass.MEASUREMENT
_attr_icon: str = "mdi:power-plug-off"

def __init__(
self,
hass: HomeAssistant,
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
name: str,
county: str,
coordinator: DataUpdateCoordinator,
key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.hass = hass
self._county = county
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
self._attr_name = name
self._attr_unique_id = f"{self._county}_{key}"
self._key = key

@property
def native_value(self) -> int:
"""Return the value of the sensor."""
return self.coordinator.data[self._key]
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
16 changes: 16 additions & 0 deletions homeassistant/components/peco/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"step": {
"user": {
"title": "PECO Outage Counter",
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
"description": "Please choose your county below.",
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
"data": {
"county": "County"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
16 changes: 16 additions & 0 deletions homeassistant/components/peco/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
IceBotYT marked this conversation as resolved.
Show resolved Hide resolved
},
"step": {
"user": {
"data": {
"county": "County"
},
"description": "Please choose your county below.",
"title": "PECO Outage Counter"
}
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
"ozw",
"p1_monitor",
"panasonic_viera",
"peco",
"philips_js",
"pi_hole",
"picnic",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,9 @@ pcal9535a==0.7
# homeassistant.components.dunehd
pdunehd==1.3.2

# homeassistant.components.peco
peco==0.0.19

# homeassistant.components.pencom
pencompy==0.0.3

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,9 @@ panasonic_viera==0.3.6
# homeassistant.components.dunehd
pdunehd==1.3.2

# homeassistant.components.peco
peco==0.0.19

# homeassistant.components.aruba
# homeassistant.components.cisco_ios
# homeassistant.components.pandora
Expand Down
1 change: 1 addition & 0 deletions tests/components/peco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the PECO Outage Counter integration."""