Compare commits

...

36 Commits

Author SHA1 Message Date
Paulus Schoutsen
4be9766498 Merge pull request #32457 from home-assistant/rc
0.106.5
2020-03-03 20:19:14 -08:00
Paulus Schoutsen
4080d6a822 Bumped version to 0.106.5 2020-03-03 18:26:19 -08:00
Robert Svensson
6d06844318 UniFi - Fix websocket bug (#32449) 2020-03-03 18:26:11 -08:00
Paulus Schoutsen
a150d6dcf3 Remove hassfest blacklisted rest (#32441)
* Remove blacklisted deps from hassfest deps

* Whitelist all internal integrations
2020-03-03 18:26:10 -08:00
Achilleas Pipinellis
a0390783bb Fix pushover's ATTR_RETRY env variable typo (#32440) 2020-03-03 18:26:10 -08:00
J. Nick Koston
91b10e875f Properly define dependency for pvoutput integration on rest in… (#32435) 2020-03-03 18:26:09 -08:00
Paulus Schoutsen
f04969cf30 Filter out duplicate logbook states (#32427) 2020-03-03 18:26:08 -08:00
Alan Tse
cdde5a37cd Fix too many device tracker updates in log for Tesla (#32426)
* Fix Tesla too many device tracker updates in log

* Empty commit to re-trigger build

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2020-03-03 18:26:07 -08:00
Paulus Schoutsen
a0403a8864 Merge pull request #32424 from home-assistant/rc
0.106.4
2020-03-02 19:41:01 -08:00
Paulus Schoutsen
bfaad97318 Add unique ID to coronavirus (#32423) 2020-03-02 18:10:51 -08:00
Paulus Schoutsen
d6c15d2f45 Bumped version to 0.106.4 2020-03-02 18:04:07 -08:00
Paulus Schoutsen
815502044e Coronavirus updates (#32417)
* Sort countries alphabetically

* Update sensor name

* Add migration to stable unique IDs

* Update sensor.py
2020-03-02 18:04:01 -08:00
mezz64
08f5b49dc4 Catch Eight Sleep API errors, don't round None type (#32410)
* Catch API errors, don't round None type

* Specify error type
2020-03-02 18:04:00 -08:00
mezz64
fab55b0ea2 Bump pyeight to 0.1.4 (#32363) 2020-03-02 18:03:59 -08:00
elmurato
649ec2fc8e Fixed TypeError with old server versions (#32329) 2020-03-02 18:03:59 -08:00
Bram Kragten
21e0df42ac Update azure-pipelines-release.yml for Azure Pipelines 2020-03-02 13:59:21 -08:00
Paulus Schoutsen
f7f9126610 Merge pull request #32414 from home-assistant/rc
0.106.3
2020-03-02 13:43:02 -08:00
Paulus Schoutsen
52809396d4 Bumped version to 0.106.3 2020-03-02 13:40:57 -08:00
Paulus Schoutsen
121d967732 Add coronavirus integration (#32413)
* Add coronavirus integration

* Update homeassistant/components/coronavirus/manifest.json

Co-Authored-By: Franck Nijhof <git@frenck.dev>

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2020-03-02 13:40:51 -08:00
Paulus Schoutsen
3b147bcbf7 Merge pull request #32327 from home-assistant/rc
0.106.2
2020-02-28 12:40:20 -08:00
J. Nick Koston
ca81c6e684 Ensure rest sensors are marked unavailable when http requests… (#32309) 2020-02-28 11:43:00 -08:00
Paulus Schoutsen
e62ba49979 Bumped version to 0.106.2 2020-02-28 11:38:53 -08:00
Robert Svensson
6ea20090a4 UniFi - Temporary workaround to get device tracker to mark cli… (#32321) 2020-02-28 11:38:37 -08:00
Paulus Schoutsen
c43b7d10d8 revent saving/deleting Lovelace config in safe mode (#32319) 2020-02-28 11:37:57 -08:00
Bram Kragten
430fa24acd Updated frontend to 20200220.5 (#32312) 2020-02-28 11:36:39 -08:00
Paulus Schoutsen
d51b2ad675 Merge pull request #32282 from home-assistant/rc
0.106.1
2020-02-27 16:37:35 -08:00
Diogo Gomes
b8fbe758d8 Bump pyipma dependency (fixes bug in 0.106) (#32286)
* Bump version

* Bump PyIPMA version
2020-02-27 14:44:22 -08:00
Paulus Schoutsen
61476f4f2c Fix dsmr test 2020-02-27 14:41:35 -08:00
Paulus Schoutsen
cab60bcd0c Bumped version to 0.106.1 2020-02-27 14:03:07 -08:00
Paulus Schoutsen
c0394232f3 Catch more Hue errors (#32275) 2020-02-27 14:03:00 -08:00
Robert Svensson
a5d9e89d08 deCONZ - Race condition on slower systems (#32274)
When battery sensors gets created before other platforms loading deconz sensors gets created first the other platform would not create entities related to those battery sensors
2020-02-27 14:02:59 -08:00
Aaron Bach
f43b26f250 Bump simplisafe-python to 9.0.2 (#32273) 2020-02-27 14:02:59 -08:00
Aaron Bach
58b32bbeff Bump simplisafe-python to 9.0.0 (#32215) 2020-02-27 14:02:58 -08:00
dupondje
6d0a465390 Fix DSMR 5 (#32233)
DSMR 5 was broken because some wrong if.
if dsmr_version in ("5B"):
-> this checks dsmr_version against 5 and B. Not if its 5B.
2020-02-27 14:00:51 -08:00
Jens Nistler
a5d334bbf7 Mark clients away if they have never been seen. (#32222) 2020-02-27 14:00:51 -08:00
Erik Montnemery
a77fd4892e Add missing translations for light actions (#32216) 2020-02-27 14:00:50 -08:00
51 changed files with 648 additions and 82 deletions

View File

@@ -68,6 +68,7 @@ homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator/* @home-assistant/core
homeassistant/components/conversation/* @home-assistant/core
homeassistant/components/coolmaster/* @OnFreund
homeassistant/components/coronavirus/* @home_assistant/core
homeassistant/components/counter/* @fabaff
homeassistant/components/cover/* @home-assistant/core
homeassistant/components/cpuspeed/* @fabaff

View File

@@ -41,7 +41,7 @@ stages:
jq curl
release="$(Build.SourceBranchName)"
created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
created_by="$(curl -s https://api.github.com/repos/home-assistant/core/releases/tags/${release} | jq --raw-output '.author.login')"
if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten|frenck)$ ]]; then
exit 0

View File

@@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"country": "Country"
},
"title": "Pick a country to monitor"
}
},
"title": "Coronavirus"
}
}

View File

@@ -0,0 +1,95 @@
"""The Coronavirus integration."""
import asyncio
from datetime import timedelta
import logging
import aiohttp
import async_timeout
import coronavirus
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import aiohttp_client, entity_registry, update_coordinator
from .const import DOMAIN
PLATFORMS = ["sensor"]
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Coronavirus component."""
# Make sure coordinator is initialized.
await get_coordinator(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Coronavirus from a config entry."""
if isinstance(entry.data["country"], int):
hass.config_entries.async_update_entry(
entry, data={**entry.data, "country": entry.title}
)
@callback
def _async_migrator(entity_entry: entity_registry.RegistryEntry):
"""Migrate away from unstable ID."""
country, info_type = entity_entry.unique_id.rsplit("-", 1)
if not country.isnumeric():
return None
return {"new_unique_id": f"{entry.title}-{info_type}"}
await entity_registry.async_migrate_entries(
hass, entry.entry_id, _async_migrator
)
if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=entry.data["country"])
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
]
)
)
return unload_ok
async def get_coordinator(hass):
"""Get the data update coordinator."""
if DOMAIN in hass.data:
return hass.data[DOMAIN]
async def async_get_cases():
try:
with async_timeout.timeout(10):
return {
case.country: case
for case in await coronavirus.get_cases(
aiohttp_client.async_get_clientsession(hass)
)
}
except (asyncio.TimeoutError, aiohttp.ClientError):
raise update_coordinator.UpdateFailed
hass.data[DOMAIN] = update_coordinator.DataUpdateCoordinator(
hass,
logging.getLogger(__name__),
name=DOMAIN,
update_method=async_get_cases,
update_interval=timedelta(hours=1),
)
await hass.data[DOMAIN].async_refresh()
return hass.data[DOMAIN]

View File

@@ -0,0 +1,45 @@
"""Config flow for Coronavirus integration."""
import logging
import voluptuous as vol
from homeassistant import config_entries
from . import get_coordinator
from .const import DOMAIN, OPTION_WORLDWIDE # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Coronavirus."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
_options = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if self._options is None:
self._options = {OPTION_WORLDWIDE: "Worldwide"}
coordinator = await get_coordinator(self.hass)
for case in sorted(
coordinator.data.values(), key=lambda case: case.country
):
self._options[case.country] = case.country
if user_input is not None:
await self.async_set_unique_id(user_input["country"])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self._options[user_input["country"]], data=user_input
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required("country"): vol.In(self._options)}),
errors=errors,
)

View File

@@ -0,0 +1,6 @@
"""Constants for the Coronavirus integration."""
from coronavirus import DEFAULT_SOURCE
DOMAIN = "coronavirus"
OPTION_WORLDWIDE = "__worldwide"
ATTRIBUTION = f"Data provided by {DEFAULT_SOURCE.NAME}"

View File

@@ -0,0 +1,12 @@
{
"domain": "coronavirus",
"name": "Coronavirus (COVID-19)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/coronavirus",
"requirements": ["coronavirus==1.0.1"],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
"codeowners": ["@home_assistant/core"]
}

View File

@@ -0,0 +1,69 @@
"""Sensor platform for the Corona virus."""
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.helpers.entity import Entity
from . import get_coordinator
from .const import ATTRIBUTION, OPTION_WORLDWIDE
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Defer sensor setup to the shared sensor module."""
coordinator = await get_coordinator(hass)
async_add_entities(
CoronavirusSensor(coordinator, config_entry.data["country"], info_type)
for info_type in ("confirmed", "recovered", "deaths", "current")
)
class CoronavirusSensor(Entity):
"""Sensor representing corona virus data."""
name = None
unique_id = None
def __init__(self, coordinator, country, info_type):
"""Initialize coronavirus sensor."""
if country == OPTION_WORLDWIDE:
self.name = f"Worldwide Coronavirus {info_type}"
else:
self.name = f"{coordinator.data[country].country} Coronavirus {info_type}"
self.unique_id = f"{country}-{info_type}"
self.coordinator = coordinator
self.country = country
self.info_type = info_type
@property
def available(self):
"""Return if sensor is available."""
return self.coordinator.last_update_success and (
self.country in self.coordinator.data or self.country == OPTION_WORLDWIDE
)
@property
def state(self):
"""State of the sensor."""
if self.country == OPTION_WORLDWIDE:
return sum(
getattr(case, self.info_type) for case in self.coordinator.data.values()
)
return getattr(self.coordinator.data[self.country], self.info_type)
@property
def unit_of_measurement(self):
"""Return unit of measurement."""
return "people"
@property
def device_state_attributes(self):
"""Return device attributes."""
return {ATTR_ATTRIBUTION: ATTRIBUTION}
async def async_added_to_hass(self):
"""When entity is added to hass."""
self.coordinator.async_add_listener(self.async_write_ha_state)
async def async_will_remove_from_hass(self):
"""When entity will be removed from hass."""
self.coordinator.async_remove_listener(self.async_write_ha_state)

View File

@@ -0,0 +1,16 @@
{
"config": {
"title": "Coronavirus",
"step": {
"user": {
"title": "Pick a country to monitor",
"data": {
"country": "Country"
}
}
},
"abort": {
"already_configured": "This country is already configured."
}
}
}

View File

@@ -37,7 +37,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzBinarySensor(sensor, gateway))

View File

@@ -44,7 +44,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzThermostat(sensor, gateway))

View File

@@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
for group in groups:
if group.lights and group.deconz_id not in gateway.deconz_ids.values():
if group.lights:
entities.append(DeconzGroup(group, gateway))
async_add_entities(entities, True)

View File

@@ -68,7 +68,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.option_allow_clip_sensor
or not sensor.type.startswith("CLIP")
)
and sensor.deconz_id not in gateway.deconz_ids.values()
):
entities.append(DeconzSensor(sensor, gateway))

View File

@@ -91,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
# Protocol version specific obis
if dsmr_version in ("4", "5"):
gas_obis = obis_ref.HOURLY_GAS_METER_READING
elif dsmr_version in ("5B"):
elif dsmr_version in ("5B",):
gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING
else:
gas_obis = obis_ref.GAS_METER_READING
@@ -238,7 +238,7 @@ class DSMREntity(Entity):
"""Convert 2/1 to normal/low depending on DSMR version."""
# DSMR V5B: Note: In Belgium values are swapped:
# Rate code 2 is used for low rate and rate code 1 is used for normal rate.
if dsmr_version in ("5B"):
if dsmr_version in ("5B",):
if value == "0001":
value = "0002"
elif value == "0002":

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings",
"requirements": [],
"dependencies": [],
"after_dependencies": ["rest"],
"codeowners": []
}

View File

@@ -2,7 +2,7 @@
"domain": "eight_sleep",
"name": "Eight Sleep",
"documentation": "https://www.home-assistant.io/integrations/eight_sleep",
"requirements": ["pyeight==0.1.3"],
"requirements": ["pyeight==0.1.4"],
"dependencies": [],
"codeowners": ["@mezz64"]
}

View File

@@ -261,14 +261,26 @@ class EightUserSensor(EightSleepUserEntity):
bed_temp = None
if "current" in self._sensor_root:
state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2)
state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2)
try:
state_attr[ATTR_RESP_RATE] = round(self._attr["resp_rate"], 2)
except TypeError:
state_attr[ATTR_RESP_RATE] = None
try:
state_attr[ATTR_HEART_RATE] = round(self._attr["heart_rate"], 2)
except TypeError:
state_attr[ATTR_HEART_RATE] = None
state_attr[ATTR_SLEEP_STAGE] = self._attr["stage"]
state_attr[ATTR_ROOM_TEMP] = room_temp
state_attr[ATTR_BED_TEMP] = bed_temp
elif "last" in self._sensor_root:
state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2)
state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2)
try:
state_attr[ATTR_AVG_RESP_RATE] = round(self._attr["resp_rate"], 2)
except TypeError:
state_attr[ATTR_AVG_RESP_RATE] = None
try:
state_attr[ATTR_AVG_HEART_RATE] = round(self._attr["heart_rate"], 2)
except TypeError:
state_attr[ATTR_AVG_HEART_RATE] = None
state_attr[ATTR_AVG_ROOM_TEMP] = room_temp
state_attr[ATTR_AVG_BED_TEMP] = bed_temp

View File

@@ -4,6 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/emulated_hue",
"requirements": ["aiohttp_cors==0.7.0"],
"dependencies": [],
"after_dependencies": ["http"],
"codeowners": [],
"quality_scale": "internal"
}

View File

@@ -3,7 +3,7 @@
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": [
"home-assistant-frontend==20200220.4"
"home-assistant-frontend==20200220.5"
],
"dependencies": [
"api",

View File

@@ -1,6 +1,7 @@
"""Code to handle a Hue bridge."""
import asyncio
from functools import partial
import logging
from aiohttp import client_exceptions
import aiohue
@@ -24,7 +25,8 @@ SCENE_SCHEMA = vol.Schema(
{vol.Required(ATTR_GROUP_NAME): cv.string, vol.Required(ATTR_SCENE_NAME): cv.string}
)
# How long should we sleep if the hub is busy
HUB_BUSY_SLEEP = 0.01
HUB_BUSY_SLEEP = 0.5
_LOGGER = logging.getLogger(__name__)
class HueBridge:
@@ -123,9 +125,14 @@ class HueBridge:
except (
client_exceptions.ClientOSError,
client_exceptions.ClientResponseError,
client_exceptions.ServerDisconnectedError,
) as err:
if tries == 3 or (
# We only retry if it's a server error. So raise on all 4XX errors.
if tries == 3:
_LOGGER.error("Request failed %s times, giving up.", tries)
raise
# We only retry if it's a server error. So raise on all 4XX errors.
if (
isinstance(err, client_exceptions.ClientResponseError)
and err.status < 500
):

View File

@@ -5,6 +5,7 @@ from functools import partial
import logging
import random
from aiohttp import client_exceptions
import aiohue
import async_timeout
@@ -172,7 +173,11 @@ async def async_safe_fetch(bridge, fetch_method):
except aiohue.Unauthorized:
await bridge.handle_unauthorized_error()
raise UpdateFailed
except (asyncio.TimeoutError, aiohue.AiohueException):
except (
asyncio.TimeoutError,
aiohue.AiohueException,
client_exceptions.ClientError,
):
raise UpdateFailed

View File

@@ -3,6 +3,7 @@ import asyncio
from datetime import timedelta
import logging
from aiohttp import client_exceptions
from aiohue import AiohueException, Unauthorized
from aiohue.sensors import TYPE_ZLL_PRESENCE
import async_timeout
@@ -60,7 +61,7 @@ class SensorManager:
except Unauthorized:
await self.bridge.handle_unauthorized_error()
raise UpdateFailed
except (asyncio.TimeoutError, AiohueException):
except (asyncio.TimeoutError, AiohueException, client_exceptions.ClientError):
raise UpdateFailed
async def async_register_component(self, binary, async_add_entities):

View File

@@ -3,7 +3,7 @@
"name": "Instituto Português do Mar e Atmosfera (IPMA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ipma",
"requirements": ["pyipma==2.0.3"],
"requirements": ["pyipma==2.0.4"],
"dependencies": [],
"codeowners": ["@dgomes", "@abmantis"]
}

View File

@@ -1,6 +1,8 @@
{
"device_automation": {
"action_type": {
"brightness_decrease": "Decrease {entity_name} brightness",
"brightness_increase": "Increase {entity_name} brightness",
"toggle": "Toggle {entity_name}",
"turn_on": "Turn on {entity_name}",
"turn_off": "Turn off {entity_name}"

View File

@@ -199,6 +199,9 @@ def humanify(hass, events):
"""
domain_prefixes = tuple(f"{dom}." for dom in CONTINUOUS_DOMAINS)
# Track last states to filter out duplicates
last_state = {}
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
events, lambda event: event.time_fired.minute // GROUP_BY_MINUTES
@@ -236,9 +239,15 @@ def humanify(hass, events):
# Yield entries
for event in events_batch:
if event.event_type == EVENT_STATE_CHANGED:
to_state = State.from_dict(event.data.get("new_state"))
# Filter out states that become same state again (force_update=True)
# or light becoming different color
if last_state.get(to_state.entity_id) == to_state.state:
continue
last_state[to_state.entity_id] = to_state.state
domain = to_state.domain
# Skip all but the last sensor state

View File

@@ -104,6 +104,9 @@ class LovelaceStorage:
async def async_save(self, config):
"""Save config."""
if self._hass.config.safe_mode:
raise HomeAssistantError("Deleting not supported in safe mode")
if self._data is None:
await self._load()
self._data["config"] = config
@@ -112,6 +115,9 @@ class LovelaceStorage:
async def async_delete(self):
"""Delete config."""
if self._hass.config.safe_mode:
raise HomeAssistantError("Deleting not supported in safe mode")
await self.async_save(None)
async def _load(self):

View File

@@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry)
await server.async_update()
server.start_periodic_update()
# Set up platform(s).
# Set up platforms.
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, platform)
@@ -103,7 +103,6 @@ class MinecraftServer:
self._mc_status = MCStatus(self.host, self.port)
# Data provided by 3rd party library
self.description = None
self.version = None
self.protocol_version = None
self.latency_time = None
@@ -168,7 +167,6 @@ class MinecraftServer:
)
# Got answer to request, update properties.
self.description = status_response.description["text"]
self.version = status_response.version.name
self.protocol_version = status_response.version.protocol
self.players_online = status_response.players.online
@@ -185,7 +183,6 @@ class MinecraftServer:
self._last_status_request_failed = False
except OSError as error:
# No answer to request, set all properties to unknown.
self.description = None
self.version = None
self.protocol_version = None
self.players_online = None

View File

@@ -61,7 +61,7 @@ class PushoverNotificationService(BaseNotificationService):
url = data.get(ATTR_URL, None)
url_title = data.get(ATTR_URL_TITLE, None)
priority = data.get(ATTR_PRIORITY, None)
retry = data.get(ATTR_PRIORITY, None)
retry = data.get(ATTR_RETRY, None)
expire = data.get(ATTR_EXPIRE, None)
callback_url = data.get(ATTR_CALLBACK_URL, None)
timestamp = data.get(ATTR_TIMESTAMP, None)

View File

@@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/pvoutput",
"requirements": [],
"dependencies": [],
"after_dependencies": ["rest"],
"codeowners": ["@fabaff"]
}

View File

@@ -202,17 +202,19 @@ class RestSensor(Entity):
self.rest.update()
value = self.rest.data
_LOGGER.debug("Data fetched from resource: %s", value)
content_type = self.rest.headers.get("content-type")
if self.rest.headers is not None:
# If the http request failed, headers will be None
content_type = self.rest.headers.get("content-type")
if content_type and content_type.startswith("text/xml"):
try:
value = json.dumps(xmltodict.parse(value))
_LOGGER.debug("JSON converted from XML: %s", value)
except ExpatError:
_LOGGER.warning(
"REST xml result could not be parsed and converted to JSON."
)
_LOGGER.debug("Erroneous XML: %s", value)
if content_type and content_type.startswith("text/xml"):
try:
value = json.dumps(xmltodict.parse(value))
_LOGGER.debug("JSON converted from XML: %s", value)
except ExpatError:
_LOGGER.warning(
"REST xml result could not be parsed and converted to JSON."
)
_LOGGER.debug("Erroneous XML: %s", value)
if self._json_attrs:
self._attributes = {}

View File

@@ -154,6 +154,7 @@ CONFIG_SCHEMA = vol.Schema(
@callback
def _async_save_refresh_token(hass, config_entry, token):
"""Save a refresh token to the config entry."""
hass.config_entries.async_update_entry(
config_entry, data={**config_entry.data, CONF_TOKEN: token}
)
@@ -547,12 +548,7 @@ class SimpliSafe:
_LOGGER.error("Unknown error while updating: %s", result)
return
if self._api.refresh_token_dirty:
# Reconnect the websocket:
await self._api.websocket.async_disconnect()
await self._api.websocket.async_connect()
# Save the new refresh token:
if self._api.refresh_token != self._config_entry.data[CONF_TOKEN]:
_async_save_refresh_token(
self._hass, self._config_entry, self._api.refresh_token
)

View File

@@ -3,7 +3,7 @@
"name": "SimpliSafe",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
"requirements": ["simplisafe-python==8.1.1"],
"requirements": ["simplisafe-python==9.0.2"],
"dependencies": [],
"codeowners": ["@bachya"]
}

View File

@@ -68,3 +68,8 @@ class TeslaDeviceEntity(TeslaDevice, TrackerEntity):
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
return SOURCE_TYPE_GPS
@property
def force_update(self):
"""All updates do not need to be written to the state machine."""
return False

View File

@@ -200,6 +200,11 @@ class UniFiClientTracker(UniFiClient, ScannerEntity):
else:
self.wired_bug = None
# A client that has never been seen cannot be connected.
if self.client.last_seen is None:
return False
since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp(
float(self.client.last_seen)
)
@@ -333,4 +338,4 @@ class UniFiDeviceTracker(ScannerEntity):
@property
def should_poll(self):
"""No polling needed."""
return False
return True

View File

@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/unifi",
"requirements": [
"aiounifi==13"
"aiounifi==14"
],
"dependencies": [],
"codeowners": [

View File

@@ -62,4 +62,4 @@ class UniFiClient(Entity):
@property
def should_poll(self) -> bool:
"""No polling needed."""
return False
return True

View File

@@ -1,7 +1,7 @@
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
MINOR_VERSION = 106
PATCH_VERSION = "0"
PATCH_VERSION = "5"
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER = (3, 7, 0)

View File

@@ -17,6 +17,7 @@ FLOWS = [
"cast",
"cert_expiry",
"coolmaster",
"coronavirus",
"daikin",
"deconz",
"dialogflow",

View File

@@ -11,7 +11,7 @@ import asyncio
from collections import OrderedDict
from itertools import chain
import logging
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, cast
import attr
@@ -560,3 +560,21 @@ def async_setup_entity_restore(
states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs)
hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states)
async def async_migrate_entries(
hass: HomeAssistantType,
config_entry_id: str,
entry_callback: Callable[[RegistryEntry], Optional[dict]],
) -> None:
"""Migrator of unique IDs."""
ent_reg = await async_get_registry(hass)
for entry in ent_reg.entities.values():
if entry.config_entry_id != config_entry_id:
continue
updates = entry_callback(entry)
if updates is not None:
ent_reg.async_update_entity(entry.entity_id, **updates) # type: ignore

View File

@@ -11,7 +11,7 @@ cryptography==2.8
defusedxml==0.6.0
distro==1.4.0
hass-nabucasa==0.31
home-assistant-frontend==20200220.4
home-assistant-frontend==20200220.5
importlib-metadata==1.5.0
jinja2>=2.10.3
netdisco==2.6.0

View File

@@ -199,7 +199,7 @@ aiopylgtv==0.3.3
aioswitcher==2019.4.26
# homeassistant.components.unifi
aiounifi==13
aiounifi==14
# homeassistant.components.wwlln
aiowwlln==2.0.2
@@ -398,6 +398,9 @@ connect-box==0.2.5
# homeassistant.components.xiaomi_miio
construct==2.9.45
# homeassistant.components.coronavirus
coronavirus==1.0.1
# homeassistant.scripts.credstash
# credstash==1.15.0
@@ -683,7 +686,7 @@ hole==0.5.0
holidays==0.10.1
# homeassistant.components.frontend
home-assistant-frontend==20200220.4
home-assistant-frontend==20200220.5
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8
@@ -1223,7 +1226,7 @@ pyeconet==0.0.11
pyedimax==0.2.1
# homeassistant.components.eight_sleep
pyeight==0.1.3
pyeight==0.1.4
# homeassistant.components.emby
pyemby==1.6
@@ -1305,7 +1308,7 @@ pyicloud==0.9.2
pyintesishome==1.6
# homeassistant.components.ipma
pyipma==2.0.3
pyipma==2.0.4
# homeassistant.components.iqvia
pyiqvia==0.2.1
@@ -1827,7 +1830,7 @@ simplehound==0.3
simplepush==1.1.4
# homeassistant.components.simplisafe
simplisafe-python==8.1.1
simplisafe-python==9.0.2
# homeassistant.components.sisyphus
sisyphus-control==2.2.1

View File

@@ -78,7 +78,7 @@ aiopylgtv==0.3.3
aioswitcher==2019.4.26
# homeassistant.components.unifi
aiounifi==13
aiounifi==14
# homeassistant.components.wwlln
aiowwlln==2.0.2
@@ -143,6 +143,9 @@ colorlog==4.1.0
# homeassistant.components.xiaomi_miio
construct==2.9.45
# homeassistant.components.coronavirus
coronavirus==1.0.1
# homeassistant.scripts.credstash
# credstash==1.15.0
@@ -254,7 +257,7 @@ hole==0.5.0
holidays==0.10.1
# homeassistant.components.frontend
home-assistant-frontend==20200220.4
home-assistant-frontend==20200220.5
# homeassistant.components.zwave
homeassistant-pyozw==0.1.8
@@ -477,7 +480,7 @@ pyhomematic==0.1.64
pyicloud==0.9.2
# homeassistant.components.ipma
pyipma==2.0.3
pyipma==2.0.4
# homeassistant.components.iqvia
pyiqvia==0.2.1
@@ -626,7 +629,7 @@ sentry-sdk==0.13.5
simplehound==0.3
# homeassistant.components.simplisafe
simplisafe-python==8.1.1
simplisafe-python==9.0.2
# homeassistant.components.sleepiq
sleepyq==0.7

View File

@@ -65,7 +65,7 @@ class ImportCollector(ast.NodeVisitor):
# self.hass.components.hue.async_create()
# Name(id=self)
# .Attribute(attr=hass)
# .Attribute(attr=hass) or .Attribute(attr=_hass)
# .Attribute(attr=hue)
# .Attribute(attr=async_create)
if (
@@ -78,7 +78,7 @@ class ImportCollector(ast.NodeVisitor):
)
or (
isinstance(node.value.value, ast.Attribute)
and node.value.value.attr == "hass"
and node.value.value.attr in ("hass", "_hass")
)
)
):
@@ -89,20 +89,47 @@ class ImportCollector(ast.NodeVisitor):
ALLOWED_USED_COMPONENTS = {
# This component will always be set up
"persistent_notification",
# These allow to register things without being set up
"conversation",
"frontend",
"hassio",
"system_health",
"websocket_api",
# Internal integrations
"alert",
"automation",
"conversation",
"device_automation",
"zone",
"frontend",
"group",
"hassio",
"homeassistant",
"system_log",
"input_boolean",
"input_datetime",
"input_number",
"input_select",
"input_text",
"persistent_notification",
"person",
"script",
"shopping_list",
"sun",
"system_health",
"system_log",
"timer",
"webhook",
"websocket_api",
"zone",
# Entity integrations with platforms
"alarm_control_panel",
"binary_sensor",
"climate",
"cover",
"device_tracker",
"fan",
"image_processing",
"light",
"lock",
"media_player",
"scene",
"sensor",
"switch",
"vacuum",
"water_heater",
# Other
"mjpeg", # base class, has no reqs or component to load.
"stream", # Stream cannot install on all systems, can be imported without reqs.
@@ -121,18 +148,7 @@ IGNORE_VIOLATIONS = {
# This should become a helper method that integrations can submit data to
("websocket_api", "lovelace"),
("websocket_api", "shopping_list"),
# Expose HA to external systems
"homekit",
"alexa",
"google_assistant",
"emulated_hue",
"prometheus",
"conversation",
"logbook",
"mobile_app",
# These should be extracted to external package
"pvoutput",
"dwd_weather_warnings",
}

View File

@@ -0,0 +1 @@
"""Tests for the Coronavirus integration."""

View File

@@ -0,0 +1,33 @@
"""Test the Coronavirus config flow."""
from asynctest import patch
from homeassistant import config_entries, setup
from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
with patch("coronavirus.get_cases", return_value=[],), patch(
"homeassistant.components.coronavirus.async_setup", return_value=True
) as mock_setup, patch(
"homeassistant.components.coronavirus.async_setup_entry", return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"country": OPTION_WORLDWIDE},
)
assert result2["type"] == "create_entry"
assert result2["title"] == "Worldwide"
assert result2["result"].unique_id == OPTION_WORLDWIDE
assert result2["data"] == {
"country": OPTION_WORLDWIDE,
}
await hass.async_block_till_done()
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1

View File

@@ -0,0 +1,58 @@
"""Test init of Coronavirus integration."""
from asynctest import Mock, patch
from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE
from homeassistant.helpers import entity_registry
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, mock_registry
async def test_migration(hass):
"""Test that we can migrate coronavirus to stable unique ID."""
nl_entry = MockConfigEntry(domain=DOMAIN, title="Netherlands", data={"country": 34})
nl_entry.add_to_hass(hass)
worldwide_entry = MockConfigEntry(
domain=DOMAIN, title="Worldwide", data={"country": OPTION_WORLDWIDE}
)
worldwide_entry.add_to_hass(hass)
mock_registry(
hass,
{
"sensor.netherlands_confirmed": entity_registry.RegistryEntry(
entity_id="sensor.netherlands_confirmed",
unique_id="34-confirmed",
platform="coronavirus",
config_entry_id=nl_entry.entry_id,
),
"sensor.worldwide_confirmed": entity_registry.RegistryEntry(
entity_id="sensor.worldwide_confirmed",
unique_id="__worldwide-confirmed",
platform="coronavirus",
config_entry_id=worldwide_entry.entry_id,
),
},
)
with patch(
"coronavirus.get_cases",
return_value=[
Mock(country="Netherlands", confirmed=10, recovered=8, deaths=1, current=1),
Mock(country="Germany", confirmed=1, recovered=0, deaths=0, current=0),
],
):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
ent_reg = await entity_registry.async_get_registry(hass)
sensor_nl = ent_reg.async_get("sensor.netherlands_confirmed")
assert sensor_nl.unique_id == "Netherlands-confirmed"
sensor_worldwide = ent_reg.async_get("sensor.worldwide_confirmed")
assert sensor_worldwide.unique_id == "__worldwide-confirmed"
assert hass.states.get("sensor.netherlands_confirmed").state == "10"
assert hass.states.get("sensor.worldwide_confirmed").state == "11"
assert nl_entry.unique_id == "Netherlands"
assert worldwide_entry.unique_id == OPTION_WORLDWIDE

View File

@@ -187,6 +187,50 @@ async def test_v4_meter(hass, mock_connection_factory):
assert gas_consumption.attributes.get("unit_of_measurement") == "m3"
async def test_v5_meter(hass, mock_connection_factory):
"""Test if v5 meter is correctly parsed."""
(connection_factory, transport, protocol) = mock_connection_factory
from dsmr_parser.obis_references import (
HOURLY_GAS_METER_READING,
ELECTRICITY_ACTIVE_TARIFF,
)
from dsmr_parser.objects import CosemObject, MBusObject
config = {"platform": "dsmr", "dsmr_version": "5"}
telegram = {
HOURLY_GAS_METER_READING: MBusObject(
[
{"value": datetime.datetime.fromtimestamp(1551642213)},
{"value": Decimal(745.695), "unit": ""},
]
),
ELECTRICITY_ACTIVE_TARIFF: CosemObject([{"value": "0001", "unit": ""}]),
}
with assert_setup_component(1):
await async_setup_component(hass, "sensor", {"sensor": config})
telegram_callback = connection_factory.call_args_list[0][0][2]
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
telegram_callback(telegram)
# after receiving telegram entities need to have the chance to update
await asyncio.sleep(0)
# tariff should be translated in human readable and have no unit
power_tariff = hass.states.get("sensor.power_tariff")
assert power_tariff.state == "low"
assert power_tariff.attributes.get("unit_of_measurement") == ""
# check if gas consumption is parsed correctly
gas_consumption = hass.states.get("sensor.gas_consumption")
assert gas_consumption.state == "745.695"
assert gas_consumption.attributes.get("unit_of_measurement") == ""
async def test_belgian_meter(hass, mock_connection_factory):
"""Test if Belgian meter is correctly parsed."""
(connection_factory, transport, protocol) = mock_connection_factory

View File

@@ -1484,3 +1484,36 @@ async def test_humanify_script_started_event(hass):
assert event2["domain"] == "script"
assert event2["message"] == "started"
assert event2["entity_id"] == "script.bye"
async def test_humanify_same_state(hass):
"""Test humanifying Script Run event."""
state_50 = ha.State("light.kitchen", "on", {"brightness": 50}).as_dict()
state_100 = ha.State("light.kitchen", "on", {"brightness": 100}).as_dict()
state_200 = ha.State("light.kitchen", "on", {"brightness": 200}).as_dict()
events = list(
logbook.humanify(
hass,
[
ha.Event(
EVENT_STATE_CHANGED,
{
"entity_id": "light.kitchen",
"old_state": state_50,
"new_state": state_100,
},
),
ha.Event(
EVENT_STATE_CHANGED,
{
"entity_id": "light.kitchen",
"old_state": state_100,
"new_state": state_200,
},
),
],
)
)
assert len(events) == 1

View File

@@ -45,6 +45,16 @@ async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage):
assert not response["success"]
assert response["error"]["code"] == "config_not_found"
await client.send_json(
{"id": 9, "type": "lovelace/config/save", "config": {"yo": "hello"}}
)
response = await client.receive_json()
assert not response["success"]
await client.send_json({"id": 10, "type": "lovelace/config/delete"})
response = await client.receive_json()
assert not response["success"]
async def test_lovelace_from_storage_save_before_load(
hass, hass_ws_client, hass_storage

View File

@@ -589,6 +589,35 @@ class TestRestSensor(unittest.TestCase):
assert mock_logger.warning.called
assert mock_logger.debug.called
@patch("homeassistant.components.rest.sensor._LOGGER")
def test_update_with_failed_get(self, mock_logger):
"""Test attributes get extracted from a XML result with bad xml."""
value_template = template("{{ value_json.toplevel.master_value }}")
value_template.hass = self.hass
self.rest.update = Mock(
"rest.RestData.update", side_effect=self.update_side_effect(None, None),
)
self.sensor = rest.RestSensor(
self.hass,
self.rest,
self.name,
self.unit_of_measurement,
self.device_class,
value_template,
["key"],
self.force_update,
self.resource_template,
self.json_attrs_path,
)
self.sensor.update()
assert {} == self.sensor.device_state_attributes
assert mock_logger.warning.called
assert mock_logger.debug.called
assert self.sensor.state is None
assert self.sensor.available is False
class TestRestData(unittest.TestCase):
"""Tests for RestData."""

View File

@@ -54,6 +54,14 @@ CLIENT_4 = {
"last_seen": 1562600145,
"mac": "00:00:00:00:00:04",
}
CLIENT_5 = {
"essid": "ssid",
"hostname": "client_5",
"ip": "10.0.0.5",
"is_wired": True,
"last_seen": None,
"mac": "00:00:00:00:00:05",
}
DEVICE_1 = {
"board_rev": 3,
@@ -111,11 +119,11 @@ async def test_tracked_devices(hass):
controller = await setup_unifi_integration(
hass,
options={CONF_SSID_FILTER: ["ssid"]},
clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, client_4_copy],
clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, CLIENT_5, client_4_copy],
devices_response=[DEVICE_1, DEVICE_2],
known_wireless_clients=(CLIENT_4["mac"],),
)
assert len(hass.states.async_all()) == 6
assert len(hass.states.async_all()) == 7
client_1 = hass.states.get("device_tracker.client_1")
assert client_1 is not None
@@ -134,6 +142,11 @@ async def test_tracked_devices(hass):
assert client_4 is not None
assert client_4.state == "not_home"
# A client that has never been seen should be marked away.
client_5 = hass.states.get("device_tracker.client_5")
assert client_5 is not None
assert client_5.state == "not_home"
device_1 = hass.states.get("device_tracker.device_1")
assert device_1 is not None
assert device_1.state == "not_home"