Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d245990
Add new integration: pvpc_hourly_pricing
azogue Feb 22, 2020
f8c15f1
Update requirements and add to codeowners
azogue Feb 22, 2020
2a62889
Avoid passing in hass as a parameter to the entity
azogue Feb 22, 2020
a76ae86
Fix lint issues
azogue Feb 22, 2020
d51d700
Add tests for config & options flow
azogue Feb 22, 2020
9bb3cbe
Add tests for manual yaml config
azogue Feb 22, 2020
f9bcff8
Fix placement of PLATFORM_SCHEMA and update generated config_flows
azogue Feb 22, 2020
6e05637
Store prices internally linked to UTC timestamps
azogue Feb 23, 2020
5de754c
Add availability to sensor
azogue Feb 23, 2020
a830b02
Add more tests
azogue Feb 23, 2020
33fa936
fix linter
azogue Feb 24, 2020
3d4d98c
Better handling of sensor availability and minor enhancements
azogue Feb 24, 2020
db82db4
Mock aiosession to not access real API, store fixture data
azogue Feb 24, 2020
977077c
Change API endpoint to retrieve JSON data
azogue Mar 4, 2020
f29a165
Adapt tests to new API endpoint
azogue Mar 4, 2020
5d866f6
Translate tariff labels to plain English
azogue Mar 4, 2020
0ac1ea7
Relax logging levels to meet silver requirements
azogue Mar 4, 2020
0e684f9
Fix requirements
azogue Mar 4, 2020
727daa2
Mod tests to work with timezone Atlantic/Canary
azogue Mar 6, 2020
d960135
Try to fix CI tests
azogue Mar 6, 2020
883c0d9
Externalize pvpc data and simplify sensor.py
azogue Mar 16, 2020
e36ab00
Simplify tests for pvpc_hourly_pricing
azogue Mar 17, 2020
a2fb561
Fix updater for options flow
azogue Mar 17, 2020
1e5324b
Fix lint
azogue Mar 17, 2020
463b107
Bump aiopvpc
azogue Mar 17, 2020
ff206c5
Remove options flow and platform setup
azogue Mar 19, 2020
30304ce
Fix docstring on test
azogue Mar 19, 2020
aa500b1
Remove timeout manual config, fix entry.options usage, simplify uniqu…
azogue Mar 20, 2020
da195fa
Simplify tests
azogue Mar 20, 2020
a64cc99
Fix possible duplicated update
azogue Mar 21, 2020
6ffc837
Do not access State last_changed for log messages
azogue Mar 21, 2020
da4e6ec
Do not update until entity is added to hass
azogue Mar 21, 2020
97f100a
minor changes
azogue Mar 21, 2020
2b53ee0
Rename method to select current price and make it a callback
azogue Mar 22, 2020
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 CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ homeassistant/components/ps4/* @ktnrg45
homeassistant/components/ptvsd/* @swamp-ig
homeassistant/components/push/* @dgomes
homeassistant/components/pvoutput/* @fabaff
homeassistant/components/pvpc_hourly_pricing/* @azogue
homeassistant/components/qld_bushfire/* @exxamalte
homeassistant/components/qnap/* @colinodell
homeassistant/components/quantum_gateway/* @cisasteelersfan
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"already_configured": "Integration is already configured with an existing sensor with that tariff"
},
"step": {
"user": {
"data": {
"name": "Sensor Name",
"tariff": "Contracted tariff (1, 2, or 3 periods)"
},
"description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)",
"title": "Tariff selection"
}
},
"title": "Hourly price of electricity in Spain (PVPC)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"already_configured": "La integraci\u00f3n ya est\u00e1 configurada con un sensor existente con esa tarifa"
},
"step": {
"user": {
"data": {
"name": "Nombre del sensor",
"tariff": "Tarifa contratada (1, 2, o 3 periodos)"
},
"description": "Este sensor utiliza la API oficial de REE para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\\u00f1a.\nPara instrucciones detalladas consulte la [documentación de la integración](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSeleccione la tarifa contratada en base al número de periodos de facturación al día:\n- 1 periodo: normal\n- 2 periodos: discriminación (tarifa nocturna)\n- 3 periodos: coche eléctrico (tarifa nocturna de 3 periodos)",
"title": "Selecci\u00f3n de tarifa"
}
},
"title": "Precio horario de la electricidad en Espa\u00f1a (PVPC)"
}
}
56 changes: 56 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""The pvpc_hourly_pricing integration to collect Spain official electric prices."""
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv

from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORM, TARIFFS

UI_CONFIG_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): str,
vol.Required(ATTR_TARIFF, default=DEFAULT_TARIFF): vol.In(TARIFFS),
}
)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}, extra=vol.ALLOW_EXTRA
)


async def async_setup(hass: HomeAssistant, config: dict):
"""
Set up the electricity price sensor from configuration.yaml.

```yaml
pvpc_hourly_pricing:
- name: PVPC manual ve
tariff: electric_car
- name: PVPC manual nocturna
tariff: discrimination
timeout: 3
```
"""
for conf in config.get(DOMAIN, []):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT}
)
)

return True


async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
"""Set up pvpc hourly pricing from a config entry."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, PLATFORM)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
"""Unload a config entry."""
return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM)
27 changes: 27 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Config flow for pvpc_hourly_pricing."""
from homeassistant import config_entries

from . import CONF_NAME, UI_CONFIG_SCHEMA
from .const import ATTR_TARIFF, DOMAIN

_DOMAIN_NAME = DOMAIN


class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME):
"""Handle a config flow for `pvpc_hourly_pricing` to select the tariff."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is not None:
await self.async_set_unique_id(user_input[ATTR_TARIFF])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_input[CONF_NAME], data=user_input)
Comment thread
bdraco marked this conversation as resolved.

return self.async_show_form(step_id="user", data_schema=UI_CONFIG_SCHEMA)

async def async_step_import(self, import_info):
"""Handle import from config file."""
return await self.async_step_user(import_info)
8 changes: 8 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Constant values for pvpc_hourly_pricing."""
from aiopvpc import TARIFFS

DOMAIN = "pvpc_hourly_pricing"
PLATFORM = "sensor"
ATTR_TARIFF = "tariff"
DEFAULT_NAME = "PVPC"
DEFAULT_TARIFF = TARIFFS[1]
10 changes: 10 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "pvpc_hourly_pricing",
"name": "Spain electricity hourly pricing (PVPC)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing",
"requirements": ["aiopvpc==1.0.2"],
"dependencies": [],
"codeowners": ["@azogue"],
"quality_scale": "platinum"
}
173 changes: 173 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""
Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.

For more details about this platform, please refer to the documentation at
https://www.home-assistant.io/integrations/pvpc_hourly_pricing/
Comment thread
azogue marked this conversation as resolved.
"""
from datetime import timedelta
import logging
from random import randint
from typing import Optional

from aiopvpc import PVPCData

from homeassistant import config_entries
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_time_change,
)
from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util

from .const import ATTR_TARIFF

_LOGGER = logging.getLogger(__name__)

ATTR_PRICE = "price"
ICON = "mdi:currency-eur"
UNIT = "€/kWh"

_DEFAULT_TIMEOUT = 10


async def async_setup_entry(
hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities
):
"""Set up the electricity price sensor from config_entry."""
name = config_entry.data[CONF_NAME]
pvpc_data_handler = PVPCData(
tariff=config_entry.data[ATTR_TARIFF],
local_timezone=hass.config.time_zone,
websession=async_get_clientsession(hass),
logger=_LOGGER,
timeout=_DEFAULT_TIMEOUT,
)
async_add_entities(
[ElecPriceSensor(name, config_entry.unique_id, pvpc_data_handler)], True
)


class ElecPriceSensor(RestoreEntity):
"""Class to hold the prices of electricity as a sensor."""

unit_of_measurement = UNIT
icon = ICON
should_poll = False

def __init__(self, name, unique_id, pvpc_data_handler):
"""Initialize the sensor object."""
self._name = name
self._unique_id = unique_id
self._pvpc_data = pvpc_data_handler
self._num_retries = 0

self._init_done = False
self._hourly_tracker = None
self._price_tracker = None

async def async_will_remove_from_hass(self) -> None:
"""Cancel listeners for sensor updates."""
self._hourly_tracker()
self._price_tracker()

async def async_added_to_hass(self):
"""Handle entity which will be added."""
await super().async_added_to_hass()
state = await self.async_get_last_state()
if state:
self._pvpc_data.state = state.state

# Update 'state' value in hour changes
self._hourly_tracker = async_track_time_change(
Comment thread
bdraco marked this conversation as resolved.
self.hass, self.async_update, second=[0], minute=[0]
)
# Update prices at random time, 2 times/hour (don't want to upset API)
random_minute = randint(1, 29)
mins_update = [random_minute, random_minute + 30]
self._price_tracker = async_track_time_change(
self.hass, self.async_update_prices, second=[0], minute=mins_update,
)
_LOGGER.debug(
"Setup of price sensor %s (%s) with tariff '%s', "
"updating prices each hour at %s min",
self.name,
self.entity_id,
self._pvpc_data.tariff,
mins_update,
)
await self.async_update_prices(dt_util.utcnow())
Comment thread
bdraco marked this conversation as resolved.
self._init_done = True
await self.async_update_ha_state(True)
Comment thread
bdraco marked this conversation as resolved.
Outdated

@property
def unique_id(self) -> Optional[str]:
"""Return a unique ID."""
return self._unique_id

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def state(self):
"""Return the state of the sensor."""
return self._pvpc_data.state

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._pvpc_data.state_available

@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._pvpc_data.attributes

async def async_update(self, *args):
"""Update the sensor state."""
if not self._init_done:
# abort until added_to_hass is finished
return

now = dt_util.utcnow() if not args else args[0]
self._pvpc_data.process_state_and_attributes(now)
self.async_write_ha_state()
Comment thread
bdraco marked this conversation as resolved.

async def async_update_prices(self, now):
"""Update electricity prices from the ESIOS API."""
prices = await self._pvpc_data.async_update_prices(now)
if not prices and self._pvpc_data.source_available:
self._num_retries += 1
if self._num_retries > 2:
_LOGGER.warning(
"Repeated bad data update, mark component as unavailable source"
)
self._pvpc_data.source_available = False
return

retry_delay = 2 * self._pvpc_data.timeout
_LOGGER.debug(
"Bad update[retry:%d], will try again in %d s",
self._num_retries,
retry_delay,
)
async_track_point_in_time(
Comment thread
azogue marked this conversation as resolved.
self.hass,
self.async_update_prices,
dt_util.now() + timedelta(seconds=retry_delay),
)
return

if not prices:
_LOGGER.debug("Data source is not yet available")
return

self._num_retries = 0
if not self._pvpc_data.source_available:
self._pvpc_data.source_available = True
_LOGGER.warning("Component has recovered data access")
self.async_schedule_update_ha_state(True)
18 changes: 18 additions & 0 deletions homeassistant/components/pvpc_hourly_pricing/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"config": {
"title": "Hourly price of electricity in Spain (PVPC)",
"step": {
"user": {
"title": "Tariff selection",
"description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)",
"data": {
"name": "Sensor Name",
"tariff": "Contracted tariff (1, 2, or 3 periods)"
}
}
},
"abort": {
"already_configured": "Integration is already configured with an existing sensor with that tariff"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"plex",
"point",
"ps4",
"pvpc_hourly_pricing",
"rachio",
"rainmachine",
"ring",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ aionotion==1.1.0
# homeassistant.components.hunterdouglas_powerview
aiopvapi==1.6.14

# homeassistant.components.pvpc_hourly_pricing
aiopvpc==1.0.2

# homeassistant.components.webostv
aiopylgtv==0.3.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 @@ -77,6 +77,9 @@ aiohue==2.0.0
# homeassistant.components.notion
aionotion==1.1.0

# homeassistant.components.pvpc_hourly_pricing
aiopvpc==1.0.2

# homeassistant.components.webostv
aiopylgtv==0.3.3

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