Move DwdWeatherWarningsAPI to a library hosted on PyPI (#34820)

* Move DwdWeatherWarningsAPI to a library hosted on PyPI
PyPI library uses new DWD WFS API
WFS API allows a more detailed query with reduced data sent as return
Change CONF_REGION_NAME from Optional to Required because it was never really optional
Set attribute region_state to "N/A" because it is not available via the new API
Add attributes warning_i_parameters and warning_i_color

* Use constants instead of raw strings
Streamline methods state and device_state_attributes

* Wrap api, use UTC time

* Update homeassistant/components/dwd_weather_warnings/sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/dwd_weather_warnings/manifest.json

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
stephan192 2020-08-11 17:55:50 +02:00 committed by GitHub
parent 5802d65ef7
commit 016cd8f8ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 81 additions and 146 deletions

View File

@ -104,6 +104,7 @@ homeassistant/components/discogs/* @thibmaek
homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/doorbird/* @oblogic7 @bdraco
homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dsmr_reader/* @depl0y
homeassistant/components/dunehd/* @bieniu homeassistant/components/dunehd/* @bieniu
homeassistant/components/dwd_weather_warnings/* @runningman84 @stephan192 @Hummel95
homeassistant/components/dweet/* @fabaff homeassistant/components/dweet/* @fabaff
homeassistant/components/dynalite/* @ziv1234 homeassistant/components/dynalite/* @ziv1234
homeassistant/components/dyson/* @etheralm homeassistant/components/dyson/* @etheralm

View File

@ -2,6 +2,6 @@
"domain": "dwd_weather_warnings", "domain": "dwd_weather_warnings",
"name": "Deutsche Wetter Dienst (DWD) Weather Warnings", "name": "Deutsche Wetter Dienst (DWD) Weather Warnings",
"documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings",
"after_dependencies": ["rest"], "codeowners": ["@runningman84", "@stephan192", "@Hummel95"],
"codeowners": [] "requirements": ["dwdwfsapi==1.0.2"]
} }

View File

@ -10,37 +10,52 @@ Warnungen vor markantem Wetter (Stufe 2)
Wetterwarnungen (Stufe 1) Wetterwarnungen (Stufe 1)
""" """
from datetime import timedelta from datetime import timedelta
import json
import logging import logging
from dwdwfsapi import DwdWeatherWarningsAPI
import voluptuous as vol import voluptuous as vol
from homeassistant.components.rest.sensor import RestData
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Data provided by DWD" ATTRIBUTION = "Data provided by DWD"
ATTR_REGION_NAME = "region_name"
ATTR_REGION_ID = "region_id"
ATTR_LAST_UPDATE = "last_update"
ATTR_WARNING_COUNT = "warning_count"
API_ATTR_WARNING_NAME = "event"
API_ATTR_WARNING_TYPE = "event_code"
API_ATTR_WARNING_LEVEL = "level"
API_ATTR_WARNING_HEADLINE = "headline"
API_ATTR_WARNING_DESCRIPTION = "description"
API_ATTR_WARNING_INSTRUCTION = "instruction"
API_ATTR_WARNING_START = "start_time"
API_ATTR_WARNING_END = "end_time"
API_ATTR_WARNING_PARAMETERS = "parameters"
API_ATTR_WARNING_COLOR = "color"
DEFAULT_NAME = "DWD-Weather-Warnings" DEFAULT_NAME = "DWD-Weather-Warnings"
CONF_REGION_NAME = "region_name" CONF_REGION_NAME = "region_name"
CURRENT_WARNING_SENSOR = "current_warning_level"
ADVANCE_WARNING_SENSOR = "advance_warning_level"
SCAN_INTERVAL = timedelta(minutes=15) SCAN_INTERVAL = timedelta(minutes=15)
MONITORED_CONDITIONS = { MONITORED_CONDITIONS = {
"current_warning_level": [ CURRENT_WARNING_SENSOR: [
"Current Warning Level", "Current Warning Level",
None, None,
"mdi:close-octagon-outline", "mdi:close-octagon-outline",
], ],
"advance_warning_level": [ ADVANCE_WARNING_SENSOR: [
"Advance Warning Level", "Advance Warning Level",
None, None,
"mdi:close-octagon-outline", "mdi:close-octagon-outline",
@ -49,7 +64,7 @@ MONITORED_CONDITIONS = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_REGION_NAME): cv.string, vol.Required(CONF_REGION_NAME): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional( vol.Optional(
CONF_MONITORED_CONDITIONS, default=list(MONITORED_CONDITIONS) CONF_MONITORED_CONDITIONS, default=list(MONITORED_CONDITIONS)
@ -63,12 +78,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
region_name = config.get(CONF_REGION_NAME) region_name = config.get(CONF_REGION_NAME)
api = DwdWeatherWarningsAPI(region_name) api = WrappedDwDWWAPI(DwdWeatherWarningsAPI(region_name))
sensors = [ sensors = []
DwdWeatherWarningsSensor(api, name, condition) for sensor_type in config[CONF_MONITORED_CONDITIONS]:
for condition in config[CONF_MONITORED_CONDITIONS] sensors.append(DwdWeatherWarningsSensor(api, name, sensor_type))
]
add_entities(sensors, True) add_entities(sensors, True)
@ -76,179 +90,96 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class DwdWeatherWarningsSensor(Entity): class DwdWeatherWarningsSensor(Entity):
"""Representation of a DWD-Weather-Warnings sensor.""" """Representation of a DWD-Weather-Warnings sensor."""
def __init__(self, api, name, variable): def __init__(self, api, name, sensor_type):
"""Initialize a DWD-Weather-Warnings sensor.""" """Initialize a DWD-Weather-Warnings sensor."""
self._api = api self._api = api
self._name = name self._name = name
self._var_id = variable self._sensor_type = sensor_type
variable_info = MONITORED_CONDITIONS[variable]
self._var_name = variable_info[0]
self._var_units = variable_info[1]
self._var_icon = variable_info[2]
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return f"{self._name} {self._var_name}" return f"{self._name} {MONITORED_CONDITIONS[self._sensor_type][0]}"
@property @property
def icon(self): def icon(self):
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend, if any."""
return self._var_icon return MONITORED_CONDITIONS[self._sensor_type][2]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._var_units return MONITORED_CONDITIONS[self._sensor_type][1]
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
try: if self._sensor_type == CURRENT_WARNING_SENSOR:
return round(self._api.data[self._var_id], 2) return self._api.api.current_warning_level
except TypeError: return self._api.api.expected_warning_level
return self._api.data[self._var_id]
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes of the DWD-Weather-Warnings.""" """Return the state attributes of the DWD-Weather-Warnings."""
data = {ATTR_ATTRIBUTION: ATTRIBUTION, "region_name": self._api.region_name} data = {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_REGION_NAME: self._api.api.warncell_name,
ATTR_REGION_ID: self._api.api.warncell_id,
ATTR_LAST_UPDATE: self._api.api.last_update,
}
if self._api.region_id is not None: if self._sensor_type == CURRENT_WARNING_SENSOR:
data["region_id"] = self._api.region_id searched_warnings = self._api.api.current_warnings
if self._api.region_state is not None:
data["region_state"] = self._api.region_state
if self._api.data["time"] is not None:
data["last_update"] = dt_util.as_local(
dt_util.utc_from_timestamp(self._api.data["time"] / 1000)
)
if self._var_id == "current_warning_level":
prefix = "current"
elif self._var_id == "advance_warning_level":
prefix = "advance"
else: else:
raise Exception("Unknown warning type") searched_warnings = self._api.api.expected_warnings
data["warning_count"] = self._api.data[f"{prefix}_warning_count"] data[ATTR_WARNING_COUNT] = len(searched_warnings)
i = 0
for event in self._api.data[f"{prefix}_warnings"]:
i = i + 1
# dictionary for the attribute containing the complete warning as json for i, warning in enumerate(searched_warnings, 1):
event_json = event.copy() data[f"warning_{i}_name"] = warning[API_ATTR_WARNING_NAME]
data[f"warning_{i}_type"] = warning[API_ATTR_WARNING_TYPE]
data[f"warning_{i}_level"] = warning[API_ATTR_WARNING_LEVEL]
data[f"warning_{i}_headline"] = warning[API_ATTR_WARNING_HEADLINE]
data[f"warning_{i}_description"] = warning[API_ATTR_WARNING_DESCRIPTION]
data[f"warning_{i}_instruction"] = warning[API_ATTR_WARNING_INSTRUCTION]
data[f"warning_{i}_start"] = warning[API_ATTR_WARNING_START]
data[f"warning_{i}_end"] = warning[API_ATTR_WARNING_END]
data[f"warning_{i}_parameters"] = warning[API_ATTR_WARNING_PARAMETERS]
data[f"warning_{i}_color"] = warning[API_ATTR_WARNING_COLOR]
data[f"warning_{i}_name"] = event["event"] # Dictionary for the attribute containing the complete warning
data[f"warning_{i}_level"] = event["level"] warning_copy = warning.copy()
data[f"warning_{i}_type"] = event["type"] warning_copy[API_ATTR_WARNING_START] = data[f"warning_{i}_start"]
if event["headline"]: warning_copy[API_ATTR_WARNING_END] = data[f"warning_{i}_end"]
data[f"warning_{i}_headline"] = event["headline"] data[f"warning_{i}"] = warning_copy
if event["description"]:
data[f"warning_{i}_description"] = event["description"]
if event["instruction"]:
data[f"warning_{i}_instruction"] = event["instruction"]
if event["start"] is not None:
data[f"warning_{i}_start"] = dt_util.as_local(
dt_util.utc_from_timestamp(event["start"] / 1000)
)
event_json["start"] = data[f"warning_{i}_start"]
if event["end"] is not None:
data[f"warning_{i}_end"] = dt_util.as_local(
dt_util.utc_from_timestamp(event["end"] / 1000)
)
event_json["end"] = data[f"warning_{i}_end"]
data[f"warning_{i}"] = event_json
return data return data
@property @property
def available(self): def available(self):
"""Could the device be accessed during the last update call.""" """Could the device be accessed during the last update call."""
return self._api.available return self._api.api.data_valid
def update(self): def update(self):
"""Get the latest data from the DWD-Weather-Warnings API.""" """Get the latest data from the DWD-Weather-Warnings API."""
_LOGGER.debug(
"Update requested for %s (%s) by %s",
self._api.api.warncell_name,
self._api.api.warncell_id,
self._sensor_type,
)
self._api.update() self._api.update()
class DwdWeatherWarningsAPI: class WrappedDwDWWAPI:
"""Get the latest data and update the states.""" """Wrapper for the DWD-Weather-Warnings api."""
def __init__(self, region_name): def __init__(self, api):
"""Initialize the data object.""" """Initialize a DWD-Weather-Warnings wrapper."""
resource = "https://www.dwd.de/DWD/warnungen/warnapp_landkreise/json/warnings.json?jsonp=loadWarnings" self.api = api
# a User-Agent is necessary for this rest api endpoint (#29496)
headers = {"User-Agent": HA_USER_AGENT}
self._rest = RestData("GET", resource, None, headers, None, True)
self.region_name = region_name
self.region_id = None
self.region_state = None
self.data = None
self.available = True
self.update()
@Throttle(SCAN_INTERVAL) @Throttle(SCAN_INTERVAL)
def update(self): def update(self):
"""Get the latest data from the DWD-Weather-Warnings.""" """Get the latest data from the DWD-Weather-Warnings API."""
try: self.api.update()
self._rest.update() _LOGGER.debug("Update performed")
json_string = self._rest.data[24 : len(self._rest.data) - 2]
json_obj = json.loads(json_string)
data = {"time": json_obj["time"]}
for mykey, myvalue in {
"current": "warnings",
"advance": "vorabInformation",
}.items():
_LOGGER.debug(
"Found %d %s global DWD warnings", len(json_obj[myvalue]), mykey
)
data[f"{mykey}_warning_level"] = 0
my_warnings = []
if self.region_id is not None:
# get a specific region_id
if self.region_id in json_obj[myvalue]:
my_warnings = json_obj[myvalue][self.region_id]
else:
# loop through all items to find warnings, region_id
# and region_state for region_name
for key in json_obj[myvalue]:
my_region = json_obj[myvalue][key][0]["regionName"]
if my_region != self.region_name:
continue
my_warnings = json_obj[myvalue][key]
my_state = json_obj[myvalue][key][0]["stateShort"]
self.region_id = key
self.region_state = my_state
break
# Get max warning level
maxlevel = data[f"{mykey}_warning_level"]
for event in my_warnings:
if event["level"] >= maxlevel:
data[f"{mykey}_warning_level"] = event["level"]
data[f"{mykey}_warning_count"] = len(my_warnings)
data[f"{mykey}_warnings"] = my_warnings
_LOGGER.debug("Found %d %s local DWD warnings", len(my_warnings), mykey)
self.data = data
self.available = True
except TypeError:
_LOGGER.error("Unable to fetch data from DWD-Weather-Warnings")
self.available = False

View File

@ -505,6 +505,9 @@ dovado==0.4.1
# homeassistant.components.dsmr # homeassistant.components.dsmr
dsmr_parser==0.18 dsmr_parser==0.18
# homeassistant.components.dwd_weather_warnings
dwdwfsapi==1.0.2
# homeassistant.components.dweet # homeassistant.components.dweet
dweepy==0.3.0 dweepy==0.3.0