Skip to content

Commit

Permalink
Switch to config_flow for Environment Canada (home-assistant#57127)
Browse files Browse the repository at this point in the history
* Add config_flow to Environment Canada

* Add unique_id

* Remove erroneous directory.

* Tests working!!

* Add back setup.

* First cut of import.

* Temp

* Tweak names.

* Import config.yaml.

* Clean up imports.

* Import working! Some refactor to clean it up.

* Add import test.

* Small optimization.

* Fix comments from code review.

* Remove CONF_NAME and config_flow for it.

* Fixup strings to match new config_flow.

* Fixes for comments from last review.

* Update tests to match new import code.

* Clean up use of CONF_TITLE; fix lint error on push.

* Phew. More cleanup on import. Really streamlined now!

* Update tests.

* Fix lint error.

* Fix lint error, try 2.

* Revert unique_id to use location as part of ID.

* Fix code review comments.

* Fix review comments.
  • Loading branch information
Glenn Waters authored Oct 11, 2021
1 parent 8ee6662 commit d0b3722
Show file tree
Hide file tree
Showing 16 changed files with 546 additions and 92 deletions.
5 changes: 4 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,10 @@ omit =
homeassistant/components/enphase_envoy/__init__.py
homeassistant/components/enphase_envoy/sensor.py
homeassistant/components/entur_public_transport/*
homeassistant/components/environment_canada/*
homeassistant/components/environment_canada/__init__.py
homeassistant/components/environment_canada/camera.py
homeassistant/components/environment_canada/sensor.py
homeassistant/components/environment_canada/weather.py
homeassistant/components/envirophat/sensor.py
homeassistant/components/envisalink/*
homeassistant/components/ephember/climate.py
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ homeassistant/components/enigma2/* @fbradyirl
homeassistant/components/enocean/* @bdurrer
homeassistant/components/enphase_envoy/* @gtdiehl
homeassistant/components/entur_public_transport/* @hfurubotten
homeassistant/components/environment_canada/* @michaeldavie
homeassistant/components/environment_canada/* @gwww @michaeldavie
homeassistant/components/ephember/* @ttroy50
homeassistant/components/epson/* @pszafer
homeassistant/components/epsonworkforce/* @ThaStealth
Expand Down
80 changes: 79 additions & 1 deletion homeassistant/components/environment_canada/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,79 @@
"""A component for Environment Canada weather."""
"""The Environment Canada (EC) component."""
from functools import partial
import logging

from env_canada import ECData, ECRadar

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE

from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN

PLATFORMS = ["camera", "sensor", "weather"]

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass, config_entry):
"""Set up EC as config entry."""
lat = config_entry.data.get(CONF_LATITUDE)
lon = config_entry.data.get(CONF_LONGITUDE)
station = config_entry.data.get(CONF_STATION)
lang = config_entry.data.get(CONF_LANGUAGE, "English")

weather_api = {}

weather_init = partial(
ECData, station_id=station, coordinates=(lat, lon), language=lang.lower()
)
weather_data = await hass.async_add_executor_job(weather_init)
weather_api["weather_data"] = weather_data

radar_init = partial(ECRadar, coordinates=(lat, lon))
radar_data = await hass.async_add_executor_job(radar_init)
weather_api["radar_data"] = radar_data
await hass.async_add_executor_job(radar_data.get_loop)

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = weather_api

hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)

return True


async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)

hass.data[DOMAIN].pop(config_entry.entry_id)

return unload_ok


def trigger_import(hass, config):
"""Trigger a import of YAML config into a config_entry."""
_LOGGER.warning(
"Environment Canada YAML configuration is deprecated; your YAML configuration "
"has been imported into the UI and can be safely removed"
)
if not config.get(CONF_LANGUAGE):
config[CONF_LANGUAGE] = "English"

data = {}
for key in (
CONF_STATION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_LANGUAGE,
): # pylint: disable=consider-using-tuple
if config.get(key):
data[key] = config[key]

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=data
)
)
63 changes: 37 additions & 26 deletions homeassistant/components/environment_canada/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from __future__ import annotations

import datetime
import logging

from env_canada import ECRadar
from env_canada import get_station_coords
from requests.exceptions import ConnectionError as RequestsConnectionError
import voluptuous as vol

from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
Expand All @@ -16,15 +18,17 @@
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle

ATTR_UPDATED = "updated"
from . import trigger_import
from .const import CONF_ATTRIBUTION, CONF_STATION, DOMAIN

CONF_ATTRIBUTION = "Data provided by Environment Canada"
CONF_STATION = "station"
CONF_LOOP = "loop"
CONF_PRECIP_TYPE = "precip_type"
ATTR_UPDATED = "updated"

MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10)

_LOGGER = logging.getLogger(__name__)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_LOOP, default=True): cv.boolean,
Expand All @@ -37,35 +41,47 @@
)


def setup_platform(hass, config, add_devices, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Environment Canada camera."""

if config.get(CONF_STATION):
radar_object = ECRadar(
station_id=config[CONF_STATION], precip_type=config.get(CONF_PRECIP_TYPE)
lat, lon = await hass.async_add_executor_job(
get_station_coords, config[CONF_STATION]
)
else:
lat = config.get(CONF_LATITUDE, hass.config.latitude)
lon = config.get(CONF_LONGITUDE, hass.config.longitude)
radar_object = ECRadar(
coordinates=(lat, lon), precip_type=config.get(CONF_PRECIP_TYPE)
)

add_devices(
[ECCamera(radar_object, config.get(CONF_NAME), config[CONF_LOOP])], True
config[CONF_LATITUDE] = lat
config[CONF_LONGITUDE] = lon

trigger_import(hass, config)


async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a weather entity from a config_entry."""
radar_data = hass.data[DOMAIN][config_entry.entry_id]["radar_data"]

async_add_entities(
[
ECCamera(
radar_data,
f"{config_entry.title} Radar",
f"{config_entry.unique_id}-radar",
),
]
)


class ECCamera(Camera):
"""Implementation of an Environment Canada radar camera."""

def __init__(self, radar_object, camera_name, is_loop):
def __init__(self, radar_object, camera_name, unique_id):
"""Initialize the camera."""
super().__init__()

self.radar_object = radar_object
self.camera_name = camera_name
self.is_loop = is_loop
self._attr_name = camera_name
self._attr_unique_id = unique_id
self.content_type = "image/gif"
self.image = None
self.timestamp = None
Expand All @@ -77,13 +93,6 @@ def camera_image(
self.update()
return self.image

@property
def name(self):
"""Return the name of the camera."""
if self.camera_name is not None:
return self.camera_name
return "Environment Canada Radar"

@property
def extra_state_attributes(self):
"""Return the state attributes of the device."""
Expand All @@ -92,8 +101,10 @@ def extra_state_attributes(self):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update radar image."""
if self.is_loop:
try:
self.image = self.radar_object.get_loop()
else:
self.image = self.radar_object.get_latest_frame()
except RequestsConnectionError:
_LOGGER.warning("Radar data update failed due to rate limiting")
return

self.timestamp = self.radar_object.timestamp
108 changes: 108 additions & 0 deletions homeassistant/components/environment_canada/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Config flow for Environment Canada integration."""
from functools import partial
import logging
import xml.etree.ElementTree as et

import aiohttp
from env_canada import ECData
import voluptuous as vol

from homeassistant import config_entries, exceptions
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import config_validation as cv

from .const import CONF_LANGUAGE, CONF_STATION, CONF_TITLE, DOMAIN

_LOGGER = logging.getLogger(__name__)


async def validate_input(hass, data):
"""Validate the user input allows us to connect."""
lat = data.get(CONF_LATITUDE)
lon = data.get(CONF_LONGITUDE)
station = data.get(CONF_STATION)
lang = data.get(CONF_LANGUAGE)

weather_init = partial(
ECData, station_id=station, coordinates=(lat, lon), language=lang.lower()
)
weather_data = await hass.async_add_executor_job(weather_init)
if weather_data.metadata.get("location") is None:
raise TooManyAttempts

if lat is None or lon is None:
lat = weather_data.lat
lon = weather_data.lon

return {
CONF_TITLE: weather_data.metadata.get("location"),
CONF_STATION: weather_data.station_id,
CONF_LATITUDE: lat,
CONF_LONGITUDE: lon,
}


class EnvironmentCanadaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Environment Canada weather."""

VERSION = 1

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except TooManyAttempts:
errors["base"] = "too_many_attempts"
except et.ParseError:
errors["base"] = "bad_station_id"
except aiohttp.ClientConnectionError:
errors["base"] = "cannot_connect"
except aiohttp.ClientResponseError as err:
if err.status == 404:
errors["base"] = "bad_station_id"
else:
errors["base"] = "error_response"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

if not errors:
user_input[CONF_STATION] = info[CONF_STATION]
user_input[CONF_LATITUDE] = info[CONF_LATITUDE]
user_input[CONF_LONGITUDE] = info[CONF_LONGITUDE]

# The combination of station and language are unique for all EC weather reporting
await self.async_set_unique_id(
f"{user_input[CONF_STATION]}-{user_input[CONF_LANGUAGE].lower()}"
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)

data_schema = vol.Schema(
{
vol.Optional(CONF_STATION): str,
vol.Optional(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Optional(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Required(CONF_LANGUAGE, default="English"): vol.In(
["English", "French"]
),
}
)

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

async def async_step_import(self, import_data):
"""Import entry from configuration.yaml."""
return await self.async_step_user(import_data)


class TooManyAttempts(exceptions.HomeAssistantError):
"""Error to indicate station ID is missing, invalid, or not in EC database."""
9 changes: 9 additions & 0 deletions homeassistant/components/environment_canada/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants for EC component."""

ATTR_OBSERVATION_TIME = "observation_time"
ATTR_STATION = "station"
CONF_ATTRIBUTION = "Data provided by Environment Canada"
CONF_LANGUAGE = "language"
CONF_STATION = "station"
CONF_TITLE = "title"
DOMAIN = "environment_canada"
5 changes: 3 additions & 2 deletions homeassistant/components/environment_canada/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"domain": "environment_canada",
"name": "Environment Canada",
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"requirements": ["env_canada==0.2.5"],
"codeowners": ["@michaeldavie"],
"requirements": ["env_canada==0.2.7"],
"codeowners": ["@gwww", "@michaeldavie"],
"config_flow": true,
"iot_class": "cloud_polling"
}
Loading

0 comments on commit d0b3722

Please sign in to comment.