Skip to content

Commit

Permalink
Add griddy integration (home-assistant#32591)
Browse files Browse the repository at this point in the history
* Add griddy integration

* Griddy is a wholesale power provider in Texas

* Supports all four load zones in Texas

* Provides real time power price which is useful for automations to handle demand response

* Update homeassistant/components/griddy/sensor.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

* Update homeassistant/components/griddy/config_flow.py

Co-Authored-By: Paulus Schoutsen <[email protected]>

* Add ability request updated via entity update service.

* Improve error message about already configured

* Remove DEVICE_CLASS_POWER since we do not have a device class for cost

* remove setdefault that was left from previous refactor

* More detail on data naming

* Bump translation for testing

* git add the config flow tests

* s/PlatformNotReady/ConfigEntryNotReady/

* Review items

* git add the other missing file

* Patch griddypower

* reduce

* adjust target

Co-authored-by: Paulus Schoutsen <[email protected]>
  • Loading branch information
bdraco and balloob authored Mar 10, 2020
1 parent 21cff00 commit 908ae23
Show file tree
Hide file tree
Showing 15 changed files with 1,012 additions and 0 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ homeassistant/components/google_translate/* @awarecan
homeassistant/components/google_travel_time/* @robbiet480
homeassistant/components/gpsd/* @fabaff
homeassistant/components/greeneye_monitor/* @jkeljo
homeassistant/components/griddy/* @bdraco
homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_server/* @indykoning
homeassistant/components/gtfs/* @robbiet480
Expand Down
21 changes: 21 additions & 0 deletions homeassistant/components/griddy/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"config" : {
"error" : {
"cannot_connect" : "Failed to connect, please try again",
"unknown" : "Unexpected error"
},
"title" : "Griddy",
"step" : {
"user" : {
"description" : "Your Load Zone is in your Griddy account under “Account > Meter > Load Zone.”",
"data" : {
"loadzone" : "Load Zone (Settlement Point)"
},
"title" : "Setup your Griddy Load Zone"
}
},
"abort" : {
"already_configured" : "This Load Zone is already configured"
}
}
}
96 changes: 96 additions & 0 deletions homeassistant/components/griddy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""The Griddy Power integration."""
import asyncio
from datetime import timedelta
import logging

from griddypower.async_api import LOAD_ZONES, AsyncGriddy
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import CONF_LOADZONE, DOMAIN, UPDATE_INTERVAL

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)})},
extra=vol.ALLOW_EXTRA,
)

PLATFORMS = ["sensor"]


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Griddy Power component."""

hass.data.setdefault(DOMAIN, {})
conf = config.get(DOMAIN)

if not conf:
return True

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_IMPORT},
data={CONF_LOADZONE: conf.get(CONF_LOADZONE)},
)
)
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Griddy Power from a config entry."""

entry_data = entry.data

async_griddy = AsyncGriddy(
aiohttp_client.async_get_clientsession(hass),
settlement_point=entry_data[CONF_LOADZONE],
)

async def async_update_data():
"""Fetch data from API endpoint."""
return await async_griddy.async_getnow()

coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name="Griddy getnow",
update_method=async_update_data,
update_interval=timedelta(seconds=UPDATE_INTERVAL),
)

await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

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

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
75 changes: 75 additions & 0 deletions homeassistant/components/griddy/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Config flow for Griddy Power integration."""
import asyncio
import logging

from aiohttp import ClientError
from griddypower.async_api import LOAD_ZONES, AsyncGriddy
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.helpers import aiohttp_client

from .const import CONF_LOADZONE
from .const import DOMAIN # pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({vol.Required(CONF_LOADZONE): vol.In(LOAD_ZONES)})


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
client_session = aiohttp_client.async_get_clientsession(hass)

try:
await AsyncGriddy(
client_session, settlement_point=data[CONF_LOADZONE]
).async_getnow()
except (asyncio.TimeoutError, ClientError):
raise CannotConnect

# Return info that you want to store in the config entry.
return {"title": f"Load Zone {data[CONF_LOADZONE]}"}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Griddy Power."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
info = None
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

if "base" not in errors:
await self.async_set_unique_id(user_input[CONF_LOADZONE])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)

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

async def async_step_import(self, user_input):
"""Handle import."""
await self.async_set_unique_id(user_input[CONF_LOADZONE])
self._abort_if_unique_id_configured()

return await self.async_step_user(user_input)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
7 changes: 7 additions & 0 deletions homeassistant/components/griddy/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Constants for the Griddy Power integration."""

DOMAIN = "griddy"

UPDATE_INTERVAL = 90

CONF_LOADZONE = "loadzone"
14 changes: 14 additions & 0 deletions homeassistant/components/griddy/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"domain": "griddy",
"name": "Griddy Power",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/griddy",
"requirements": ["griddypower==0.1.0"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": [
"@bdraco"
]
}
76 changes: 76 additions & 0 deletions homeassistant/components/griddy/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Support for August sensors."""
import logging

from homeassistant.helpers.entity import Entity

from .const import CONF_LOADZONE, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the August sensors."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]

settlement_point = config_entry.data[CONF_LOADZONE]

async_add_entities([GriddyPriceSensor(settlement_point, coordinator)], True)


class GriddyPriceSensor(Entity):
"""Representation of an August sensor."""

def __init__(self, settlement_point, coordinator):
"""Initialize the sensor."""
self._coordinator = coordinator
self._settlement_point = settlement_point

@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return "¢/kWh"

@property
def name(self):
"""Device Name."""
return f"{self._settlement_point} Price Now"

@property
def icon(self):
"""Device Ice."""
return "mdi:currency-usd"

@property
def unique_id(self):
"""Device Uniqueid."""
return f"{self._settlement_point}_price_now"

@property
def available(self):
"""Return True if entity is available."""
return self._coordinator.last_update_success

@property
def state(self):
"""Get the current price."""
return round(float(self._coordinator.data.now.price_cents_kwh), 4)

@property
def should_poll(self):
"""Return False, updates are controlled via coordinator."""
return False

async def async_update(self):
"""Update the entity.
Only used by the generic entity update service.
"""
await self._coordinator.async_request_refresh()

async def async_added_to_hass(self):
"""Subscribe to updates."""
self._coordinator.async_add_listener(self.async_write_ha_state)

async def async_will_remove_from_hass(self):
"""Undo subscription."""
self._coordinator.async_remove_listener(self.async_write_ha_state)
21 changes: 21 additions & 0 deletions homeassistant/components/griddy/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"config" : {
"error" : {
"cannot_connect" : "Failed to connect, please try again",
"unknown" : "Unexpected error"
},
"title" : "Griddy",
"step" : {
"user" : {
"description" : "Your Load Zone is in your Griddy account under “Account > Meter > Load Zone.”",
"data" : {
"loadzone" : "Load Zone (Settlement Point)"
},
"title" : "Setup your Griddy Load Zone"
}
},
"abort" : {
"already_configured" : "This Load Zone is already configured"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"gios",
"glances",
"gpslogger",
"griddy",
"hangouts",
"heos",
"hisense_aehw4a1",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ greeneye_monitor==2.0
# homeassistant.components.greenwave
greenwavereality==0.5.1

# homeassistant.components.griddy
griddypower==0.1.0

# homeassistant.components.growatt_server
growattServer==0.0.1

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ google-api-python-client==1.6.4
# homeassistant.components.google_pubsub
google-cloud-pubsub==0.39.1

# homeassistant.components.griddy
griddypower==0.1.0

# homeassistant.components.ffmpeg
ha-ffmpeg==2.0

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

0 comments on commit 908ae23

Please sign in to comment.