mirror of
https://github.com/home-assistant/core.git
synced 2026-01-07 16:17:18 +01:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c1da182e2 | ||
|
|
39bdb562d3 | ||
|
|
1d1f8df509 | ||
|
|
ccb8b6b9c8 | ||
|
|
c0dcd9c674 | ||
|
|
6062854666 | ||
|
|
4be9766498 | ||
|
|
4080d6a822 | ||
|
|
6d06844318 | ||
|
|
a150d6dcf3 | ||
|
|
a0390783bb | ||
|
|
91b10e875f | ||
|
|
f04969cf30 | ||
|
|
cdde5a37cd | ||
|
|
a0403a8864 | ||
|
|
bfaad97318 | ||
|
|
d6c15d2f45 | ||
|
|
815502044e | ||
|
|
08f5b49dc4 | ||
|
|
fab55b0ea2 | ||
|
|
649ec2fc8e | ||
|
|
21e0df42ac | ||
|
|
f7f9126610 | ||
|
|
52809396d4 | ||
|
|
121d967732 | ||
|
|
3b147bcbf7 | ||
|
|
ca81c6e684 | ||
|
|
e62ba49979 | ||
|
|
6ea20090a4 | ||
|
|
c43b7d10d8 | ||
|
|
430fa24acd | ||
|
|
d51b2ad675 | ||
|
|
b8fbe758d8 | ||
|
|
61476f4f2c | ||
|
|
cab60bcd0c | ||
|
|
c0394232f3 | ||
|
|
a5d9e89d08 | ||
|
|
f43b26f250 | ||
|
|
58b32bbeff | ||
|
|
6d0a465390 | ||
|
|
a5d334bbf7 | ||
|
|
a77fd4892e |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
homeassistant/components/coronavirus/.translations/en.json
Normal file
13
homeassistant/components/coronavirus/.translations/en.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "Country"
|
||||
},
|
||||
"title": "Pick a country to monitor"
|
||||
}
|
||||
},
|
||||
"title": "Coronavirus"
|
||||
}
|
||||
}
|
||||
95
homeassistant/components/coronavirus/__init__.py
Normal file
95
homeassistant/components/coronavirus/__init__.py
Normal 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]
|
||||
45
homeassistant/components/coronavirus/config_flow.py
Normal file
45
homeassistant/components/coronavirus/config_flow.py
Normal 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,
|
||||
)
|
||||
6
homeassistant/components/coronavirus/const.py
Normal file
6
homeassistant/components/coronavirus/const.py
Normal 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}"
|
||||
12
homeassistant/components/coronavirus/manifest.json
Normal file
12
homeassistant/components/coronavirus/manifest.json
Normal 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.1.0"],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": ["@home_assistant/core"]
|
||||
}
|
||||
69
homeassistant/components/coronavirus/sensor.py
Normal file
69
homeassistant/components/coronavirus/sensor.py
Normal 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)
|
||||
16
homeassistant/components/coronavirus/strings.json
Normal file
16
homeassistant/components/coronavirus/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"after_dependencies": ["rest"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -97,7 +97,12 @@ class FacebookNotificationService(BaseNotificationService):
|
||||
else:
|
||||
recipient = {"id": target}
|
||||
|
||||
body = {"recipient": recipient, "message": body_message}
|
||||
body = {
|
||||
"recipient": recipient,
|
||||
"message": body_message,
|
||||
"messaging_type": "MESSAGE_TAG",
|
||||
"tag": "ACCOUNT_UPDATE",
|
||||
}
|
||||
resp = requests.post(
|
||||
BASE_URL,
|
||||
data=json.dumps(body),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -123,10 +123,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool
|
||||
hass, username, password, icloud_dir, max_interval, gps_accuracy_threshold,
|
||||
)
|
||||
await hass.async_add_executor_job(account.setup)
|
||||
if not account.devices:
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN][username] = account
|
||||
hass.data[DOMAIN][entry.unique_id] = account
|
||||
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
|
||||
@@ -10,6 +10,7 @@ from pyicloud.services.findmyiphone import AppleDevice
|
||||
|
||||
from homeassistant.components.zone import async_active_zone
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -37,7 +38,7 @@ from .const import (
|
||||
DEVICE_STATUS,
|
||||
DEVICE_STATUS_CODES,
|
||||
DEVICE_STATUS_SET,
|
||||
SERVICE_UPDATE,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
ATTRIBUTION = "Data provided by Apple iCloud"
|
||||
@@ -91,7 +92,7 @@ class IcloudAccount:
|
||||
self._family_members_fullname = {}
|
||||
self._devices = {}
|
||||
|
||||
self.unsub_device_tracker = None
|
||||
self.listeners = []
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up an iCloud account."""
|
||||
@@ -104,13 +105,17 @@ class IcloudAccount:
|
||||
_LOGGER.error("Error logging into iCloud Service: %s", error)
|
||||
return
|
||||
|
||||
user_info = None
|
||||
try:
|
||||
api_devices = self.api.devices
|
||||
# Gets device owners infos
|
||||
user_info = self.api.devices.response["userInfo"]
|
||||
except PyiCloudNoDevicesException:
|
||||
user_info = api_devices.response["userInfo"]
|
||||
except (KeyError, PyiCloudNoDevicesException):
|
||||
_LOGGER.error("No iCloud device found")
|
||||
return
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
|
||||
_LOGGER.warning("Pending devices, trying again ...")
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}"
|
||||
|
||||
@@ -132,13 +137,21 @@ class IcloudAccount:
|
||||
api_devices = {}
|
||||
try:
|
||||
api_devices = self.api.devices
|
||||
except PyiCloudNoDevicesException:
|
||||
_LOGGER.error("No iCloud device found")
|
||||
return
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
_LOGGER.error("Unknown iCloud error: %s", err)
|
||||
self._fetch_interval = 5
|
||||
dispatcher_send(self.hass, SERVICE_UPDATE)
|
||||
self._fetch_interval = 2
|
||||
dispatcher_send(self.hass, self.signal_device_update)
|
||||
track_point_in_utc_time(
|
||||
self.hass,
|
||||
self.keep_alive,
|
||||
utcnow() + timedelta(minutes=self._fetch_interval),
|
||||
)
|
||||
return
|
||||
|
||||
if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending":
|
||||
_LOGGER.warning("Pending devices, trying again in 15s")
|
||||
self._fetch_interval = 0.25
|
||||
dispatcher_send(self.hass, self.signal_device_update)
|
||||
track_point_in_utc_time(
|
||||
self.hass,
|
||||
self.keep_alive,
|
||||
@@ -147,10 +160,19 @@ class IcloudAccount:
|
||||
return
|
||||
|
||||
# Gets devices infos
|
||||
new_device = False
|
||||
for device in api_devices:
|
||||
status = device.status(DEVICE_STATUS_SET)
|
||||
device_id = status[DEVICE_ID]
|
||||
device_name = status[DEVICE_NAME]
|
||||
device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error")
|
||||
|
||||
if (
|
||||
device_status == "pending"
|
||||
or status[DEVICE_BATTERY_STATUS] == "Unknown"
|
||||
or status.get(DEVICE_BATTERY_LEVEL) is None
|
||||
):
|
||||
continue
|
||||
|
||||
if self._devices.get(device_id, None) is not None:
|
||||
# Seen device -> updating
|
||||
@@ -165,9 +187,14 @@ class IcloudAccount:
|
||||
)
|
||||
self._devices[device_id] = IcloudDevice(self, device, status)
|
||||
self._devices[device_id].update(status)
|
||||
new_device = True
|
||||
|
||||
self._fetch_interval = self._determine_interval()
|
||||
dispatcher_send(self.hass, SERVICE_UPDATE)
|
||||
|
||||
dispatcher_send(self.hass, self.signal_device_update)
|
||||
if new_device:
|
||||
dispatcher_send(self.hass, self.signal_device_new)
|
||||
|
||||
track_point_in_utc_time(
|
||||
self.hass,
|
||||
self.keep_alive,
|
||||
@@ -291,6 +318,16 @@ class IcloudAccount:
|
||||
"""Return the account devices."""
|
||||
return self._devices
|
||||
|
||||
@property
|
||||
def signal_device_new(self) -> str:
|
||||
"""Event specific per Freebox entry to signal new device."""
|
||||
return f"{DOMAIN}-{self._username}-device-new"
|
||||
|
||||
@property
|
||||
def signal_device_update(self) -> str:
|
||||
"""Event specific per Freebox entry to signal updates in devices."""
|
||||
return f"{DOMAIN}-{self._username}-device-update"
|
||||
|
||||
|
||||
class IcloudDevice:
|
||||
"""Representation of a iCloud device."""
|
||||
@@ -348,6 +385,8 @@ class IcloudDevice:
|
||||
and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE]
|
||||
):
|
||||
location = self._status[DEVICE_LOCATION]
|
||||
if self._location is None:
|
||||
dispatcher_send(self._account.hass, self._account.signal_device_new)
|
||||
self._location = location
|
||||
|
||||
def play_sound(self) -> None:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""iCloud component constants."""
|
||||
|
||||
DOMAIN = "icloud"
|
||||
SERVICE_UPDATE = f"{DOMAIN}_update"
|
||||
|
||||
CONF_MAX_INTERVAL = "max_interval"
|
||||
CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold"
|
||||
|
||||
@@ -5,17 +5,16 @@ from typing import Dict
|
||||
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
|
||||
from homeassistant.components.device_tracker.config_entry import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .account import IcloudDevice
|
||||
from .account import IcloudAccount, IcloudDevice
|
||||
from .const import (
|
||||
DEVICE_LOCATION_HORIZONTAL_ACCURACY,
|
||||
DEVICE_LOCATION_LATITUDE,
|
||||
DEVICE_LOCATION_LONGITUDE,
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -30,25 +29,45 @@ async def async_setup_scanner(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
):
|
||||
"""Configure a dispatcher connection based on a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
) -> None:
|
||||
"""Set up device tracker for iCloud component."""
|
||||
account = hass.data[DOMAIN][entry.unique_id]
|
||||
tracked = set()
|
||||
|
||||
for device in hass.data[DOMAIN][username].devices.values():
|
||||
if device.location is None:
|
||||
_LOGGER.debug("No position found for %s", device.name)
|
||||
@callback
|
||||
def update_account():
|
||||
"""Update the values of the account."""
|
||||
add_entities(account, async_add_entities, tracked)
|
||||
|
||||
account.listeners.append(
|
||||
async_dispatcher_connect(hass, account.signal_device_new, update_account)
|
||||
)
|
||||
|
||||
update_account()
|
||||
|
||||
|
||||
@callback
|
||||
def add_entities(account, async_add_entities, tracked):
|
||||
"""Add new tracker entities from the account."""
|
||||
new_tracked = []
|
||||
|
||||
for dev_id, device in account.devices.items():
|
||||
if dev_id in tracked or device.location is None:
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Adding device_tracker for %s", device.name)
|
||||
new_tracked.append(IcloudTrackerEntity(account, device))
|
||||
tracked.add(dev_id)
|
||||
|
||||
async_add_entities([IcloudTrackerEntity(device)])
|
||||
if new_tracked:
|
||||
async_add_entities(new_tracked, True)
|
||||
|
||||
|
||||
class IcloudTrackerEntity(TrackerEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
def __init__(self, device: IcloudDevice):
|
||||
def __init__(self, account: IcloudAccount, device: IcloudDevice):
|
||||
"""Set up the iCloud tracker entity."""
|
||||
self._account = account
|
||||
self._device = device
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@@ -115,7 +134,7 @@ class IcloudTrackerEntity(TrackerEntity):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register state update callback."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, SERVICE_UPDATE, self.async_write_ha_state
|
||||
self.hass, self._account.signal_device_update, self.async_write_ha_state
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
|
||||
@@ -3,14 +3,15 @@ import logging
|
||||
from typing import Dict
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY
|
||||
from homeassistant.const import DEVICE_CLASS_BATTERY
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from .account import IcloudDevice
|
||||
from .const import DOMAIN, SERVICE_UPDATE
|
||||
from .account import IcloudAccount, IcloudDevice
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,23 +19,44 @@ _LOGGER = logging.getLogger(__name__)
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
|
||||
) -> None:
|
||||
"""Set up iCloud devices sensors based on a config entry."""
|
||||
username = entry.data[CONF_USERNAME]
|
||||
"""Set up device tracker for iCloud component."""
|
||||
account = hass.data[DOMAIN][entry.unique_id]
|
||||
tracked = set()
|
||||
|
||||
entities = []
|
||||
for device in hass.data[DOMAIN][username].devices.values():
|
||||
if device.battery_level is not None:
|
||||
_LOGGER.debug("Adding battery sensor for %s", device.name)
|
||||
entities.append(IcloudDeviceBatterySensor(device))
|
||||
@callback
|
||||
def update_account():
|
||||
"""Update the values of the account."""
|
||||
add_entities(account, async_add_entities, tracked)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
account.listeners.append(
|
||||
async_dispatcher_connect(hass, account.signal_device_new, update_account)
|
||||
)
|
||||
|
||||
update_account()
|
||||
|
||||
|
||||
@callback
|
||||
def add_entities(account, async_add_entities, tracked):
|
||||
"""Add new tracker entities from the account."""
|
||||
new_tracked = []
|
||||
|
||||
for dev_id, device in account.devices.items():
|
||||
if dev_id in tracked or device.battery_level is None:
|
||||
continue
|
||||
|
||||
new_tracked.append(IcloudDeviceBatterySensor(account, device))
|
||||
tracked.add(dev_id)
|
||||
|
||||
if new_tracked:
|
||||
async_add_entities(new_tracked, True)
|
||||
|
||||
|
||||
class IcloudDeviceBatterySensor(Entity):
|
||||
"""Representation of a iCloud device battery sensor."""
|
||||
|
||||
def __init__(self, device: IcloudDevice):
|
||||
def __init__(self, account: IcloudAccount, device: IcloudDevice):
|
||||
"""Initialize the battery sensor."""
|
||||
self._account = account
|
||||
self._device = device
|
||||
self._unsub_dispatcher = None
|
||||
|
||||
@@ -94,7 +116,7 @@ class IcloudDeviceBatterySensor(Entity):
|
||||
async def async_added_to_hass(self):
|
||||
"""Register state update callback."""
|
||||
self._unsub_dispatcher = async_dispatcher_connect(
|
||||
self.hass, SERVICE_UPDATE, self.async_write_ha_state
|
||||
self.hass, self._account.signal_device_update, self.async_write_ha_state
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pvoutput",
|
||||
"requirements": [],
|
||||
"dependencies": [],
|
||||
"after_dependencies": ["rest"],
|
||||
"codeowners": ["@fabaff"]
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifi",
|
||||
"requirements": [
|
||||
"aiounifi==13"
|
||||
"aiounifi==14"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
|
||||
@@ -62,4 +62,4 @@ class UniFiClient(Entity):
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling needed."""
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "velbus",
|
||||
"name": "Velbus",
|
||||
"documentation": "https://www.home-assistant.io/integrations/velbus",
|
||||
"requirements": ["python-velbus==2.0.41"],
|
||||
"requirements": ["python-velbus==2.0.42"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"codeowners": ["@Cereal2nd", "@brefra"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 106
|
||||
PATCH_VERSION = "0"
|
||||
PATCH_VERSION = "6"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
||||
@@ -17,6 +17,7 @@ FLOWS = [
|
||||
"cast",
|
||||
"cert_expiry",
|
||||
"coolmaster",
|
||||
"coronavirus",
|
||||
"daikin",
|
||||
"deconz",
|
||||
"dialogflow",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.1.0
|
||||
|
||||
# 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
|
||||
@@ -1640,7 +1643,7 @@ python-telnet-vlc==1.0.4
|
||||
python-twitch-client==0.6.0
|
||||
|
||||
# homeassistant.components.velbus
|
||||
python-velbus==2.0.41
|
||||
python-velbus==2.0.42
|
||||
|
||||
# homeassistant.components.vlc
|
||||
python-vlc==1.1.2
|
||||
@@ -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
|
||||
|
||||
@@ -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.1.0
|
||||
|
||||
# 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
|
||||
@@ -578,7 +581,7 @@ python-nest==4.1.0
|
||||
python-twitch-client==0.6.0
|
||||
|
||||
# homeassistant.components.velbus
|
||||
python-velbus==2.0.41
|
||||
python-velbus==2.0.42
|
||||
|
||||
# homeassistant.components.awair
|
||||
python_awair==0.0.4
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
1
tests/components/coronavirus/__init__.py
Normal file
1
tests/components/coronavirus/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Coronavirus integration."""
|
||||
33
tests/components/coronavirus/test_config_flow.py
Normal file
33
tests/components/coronavirus/test_config_flow.py
Normal 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
|
||||
58
tests/components/coronavirus/test_init.py
Normal file
58
tests/components/coronavirus/test_init.py
Normal 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
|
||||
@@ -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": "m³"},
|
||||
]
|
||||
),
|
||||
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") == "m³"
|
||||
|
||||
|
||||
async def test_belgian_meter(hass, mock_connection_factory):
|
||||
"""Test if Belgian meter is correctly parsed."""
|
||||
(connection_factory, transport, protocol) = mock_connection_factory
|
||||
|
||||
@@ -30,6 +30,8 @@ class TestFacebook(unittest.TestCase):
|
||||
expected_body = {
|
||||
"recipient": {"phone_number": target[0]},
|
||||
"message": {"text": message},
|
||||
"messaging_type": "MESSAGE_TAG",
|
||||
"tag": "ACCOUNT_UPDATE",
|
||||
}
|
||||
assert mock.last_request.json() == expected_body
|
||||
|
||||
@@ -53,6 +55,8 @@ class TestFacebook(unittest.TestCase):
|
||||
expected_body = {
|
||||
"recipient": {"phone_number": target},
|
||||
"message": {"text": message},
|
||||
"messaging_type": "MESSAGE_TAG",
|
||||
"tag": "ACCOUNT_UPDATE",
|
||||
}
|
||||
assert request.json() == expected_body
|
||||
|
||||
@@ -77,7 +81,12 @@ class TestFacebook(unittest.TestCase):
|
||||
assert mock.called
|
||||
assert mock.call_count == 1
|
||||
|
||||
expected_body = {"recipient": {"phone_number": target[0]}, "message": data}
|
||||
expected_body = {
|
||||
"recipient": {"phone_number": target[0]},
|
||||
"message": data,
|
||||
"messaging_type": "MESSAGE_TAG",
|
||||
"tag": "ACCOUNT_UPDATE",
|
||||
}
|
||||
assert mock.last_request.json() == expected_body
|
||||
|
||||
expected_params = {"access_token": ["page-access-token"]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user