mirror of
https://github.com/home-assistant/core.git
synced 2026-05-03 19:41:15 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0be9aca48 | |||
| cb51a00c37 | |||
| 6f5884805e | |||
| 65450d8518 | |||
| f1e3023d44 | |||
| aa4e879e1a | |||
| 43961dc36b | |||
| 24c3cbfff9 | |||
| 95ffe12264 | |||
| eac5619001 | |||
| ab9df350fd | |||
| a58a67923b | |||
| a92bc562d3 | |||
| 214bc81d02 | |||
| b626368b6a | |||
| 30e1ff83b9 | |||
| 6e31a2e67d | |||
| 64749a0f85 | |||
| 6baded622b | |||
| 790a136c0a | |||
| 4fc56cec1c | |||
| 93cdd4dbf3 | |||
| f0f112ff42 |
@@ -114,7 +114,7 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
async def _async_update_data(self):
|
||||
"""Update data via library."""
|
||||
try:
|
||||
with timeout(10):
|
||||
async with timeout(10):
|
||||
current = await self.accuweather.async_get_current_conditions()
|
||||
forecast = (
|
||||
await self.accuweather.async_get_forecast(metric=self.is_metric)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "doorbird",
|
||||
"name": "DoorBird",
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"requirements": ["doorbirdpy==2.0.8"],
|
||||
"requirements": ["doorbirdpy==2.1.0"],
|
||||
"dependencies": ["http"],
|
||||
"zeroconf": ["_axis-video._tcp.local."],
|
||||
"codeowners": ["@oblogic7", "@bdraco"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20200805.0"],
|
||||
"requirements": ["home-assistant-frontend==20200807.1"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -1267,46 +1267,49 @@ class ModesTrait(_Trait):
|
||||
|
||||
return features & media_player.SUPPORT_SELECT_SOUND_MODE
|
||||
|
||||
def _generate(self, name, settings):
|
||||
"""Generate a list of modes."""
|
||||
mode = {
|
||||
"name": name,
|
||||
"name_values": [
|
||||
{"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"}
|
||||
],
|
||||
"settings": [],
|
||||
"ordered": False,
|
||||
}
|
||||
for setting in settings:
|
||||
mode["settings"].append(
|
||||
{
|
||||
"setting_name": setting,
|
||||
"setting_values": [
|
||||
{
|
||||
"setting_synonym": self.SYNONYMS.get(setting, [setting]),
|
||||
"lang": "en",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
return mode
|
||||
|
||||
def sync_attributes(self):
|
||||
"""Return mode attributes for a sync request."""
|
||||
|
||||
def _generate(name, settings):
|
||||
mode = {
|
||||
"name": name,
|
||||
"name_values": [
|
||||
{"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"}
|
||||
],
|
||||
"settings": [],
|
||||
"ordered": False,
|
||||
}
|
||||
for setting in settings:
|
||||
mode["settings"].append(
|
||||
{
|
||||
"setting_name": setting,
|
||||
"setting_values": [
|
||||
{
|
||||
"setting_synonym": self.SYNONYMS.get(
|
||||
setting, [setting]
|
||||
),
|
||||
"lang": "en",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
return mode
|
||||
|
||||
attrs = self.state.attributes
|
||||
modes = []
|
||||
if self.state.domain == media_player.DOMAIN:
|
||||
if media_player.ATTR_SOUND_MODE_LIST in attrs:
|
||||
modes.append(
|
||||
_generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST])
|
||||
)
|
||||
elif self.state.domain == input_select.DOMAIN:
|
||||
modes.append(_generate("option", attrs[input_select.ATTR_OPTIONS]))
|
||||
elif self.state.domain == humidifier.DOMAIN:
|
||||
if humidifier.ATTR_AVAILABLE_MODES in attrs:
|
||||
modes.append(_generate("mode", attrs[humidifier.ATTR_AVAILABLE_MODES]))
|
||||
|
||||
for domain, attr, name in (
|
||||
(media_player.DOMAIN, media_player.ATTR_SOUND_MODE_LIST, "sound mode"),
|
||||
(input_select.DOMAIN, input_select.ATTR_OPTIONS, "option"),
|
||||
(humidifier.DOMAIN, humidifier.ATTR_AVAILABLE_MODES, "mode"),
|
||||
):
|
||||
if self.state.domain != domain:
|
||||
continue
|
||||
|
||||
items = self.state.attributes.get(attr)
|
||||
|
||||
if items is not None:
|
||||
modes.append(self._generate(name, items))
|
||||
|
||||
# Shortcut since all domains are currently unique
|
||||
break
|
||||
|
||||
payload = {"availableModes": modes}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class GuardianBinarySensor(GuardianEntity, BinarySensorEntity):
|
||||
def _async_update_from_latest_data(self) -> None:
|
||||
"""Update the entity."""
|
||||
if self._kind == SENSOR_KIND_AP_INFO:
|
||||
self._is_on = self._coordinators[API_WIFI_STATUS].data["ap_enabled"]
|
||||
self._is_on = self._coordinators[API_WIFI_STATUS].data["station_connected"]
|
||||
self._attrs.update(
|
||||
{
|
||||
ATTR_CONNECTED_CLIENTS: self._coordinators[API_WIFI_STATUS].data[
|
||||
|
||||
@@ -129,6 +129,7 @@ SERV_OUTLET = "Outlet"
|
||||
SERV_SECURITY_SYSTEM = "SecuritySystem"
|
||||
SERV_SMOKE_SENSOR = "SmokeSensor"
|
||||
SERV_SPEAKER = "Speaker"
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH = "StatelessProgrammableSwitch"
|
||||
SERV_SWITCH = "Switch"
|
||||
SERV_TELEVISION = "Television"
|
||||
SERV_TELEVISION_SPEAKER = "TelevisionSpeaker"
|
||||
|
||||
@@ -54,6 +54,7 @@ from .const import (
|
||||
SERV_DOORBELL,
|
||||
SERV_MOTION_SENSOR,
|
||||
SERV_SPEAKER,
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||
)
|
||||
from .img_util import scale_jpeg_camera_image
|
||||
from .util import pid_is_alive
|
||||
@@ -211,6 +212,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
self._async_update_motion_state(state)
|
||||
|
||||
self._char_doorbell_detected = None
|
||||
self._char_doorbell_detected_switch = None
|
||||
self.linked_doorbell_sensor = self.config.get(CONF_LINKED_DOORBELL_SENSOR)
|
||||
if self.linked_doorbell_sensor:
|
||||
state = self.hass.states.get(self.linked_doorbell_sensor)
|
||||
@@ -220,6 +222,14 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
self._char_doorbell_detected = serv_doorbell.configure_char(
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT, value=0,
|
||||
)
|
||||
serv_stateless_switch = self.add_preload_service(
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH
|
||||
)
|
||||
self._char_doorbell_detected_switch = serv_stateless_switch.configure_char(
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||
value=0,
|
||||
valid_values={"SinglePress": DOORBELL_SINGLE_PRESS},
|
||||
)
|
||||
serv_speaker = self.add_preload_service(SERV_SPEAKER)
|
||||
serv_speaker.configure_char(CHAR_MUTE, value=0)
|
||||
|
||||
@@ -282,6 +292,7 @@ class Camera(HomeAccessory, PyhapCamera):
|
||||
|
||||
if new_state.state == STATE_ON:
|
||||
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
|
||||
self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS)
|
||||
_LOGGER.debug(
|
||||
"%s: Set linked doorbell %s sensor to %d",
|
||||
self.entity_id,
|
||||
|
||||
@@ -354,7 +354,7 @@ def show_setup_message(hass, entry_id, bridge_name, pincode, uri):
|
||||
|
||||
buffer = io.BytesIO()
|
||||
url = pyqrcode.create(uri)
|
||||
url.svg(buffer, scale=5)
|
||||
url.svg(buffer, scale=5, module_color="#000", background="#FFF")
|
||||
pairing_secret = secrets.token_hex(32)
|
||||
|
||||
hass.data[DOMAIN][entry_id][HOMEKIT_PAIRING_QR] = buffer.getvalue()
|
||||
|
||||
@@ -11,6 +11,7 @@ from aiohomekit.model.characteristics import (
|
||||
)
|
||||
from aiohomekit.model.services import Service, ServicesTypes
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -212,7 +213,8 @@ async def async_setup(hass, config):
|
||||
map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass)
|
||||
await map_storage.async_initialize()
|
||||
|
||||
hass.data[CONTROLLER] = aiohomekit.Controller()
|
||||
zeroconf_instance = await zeroconf.async_get_instance(hass)
|
||||
hass.data[CONTROLLER] = aiohomekit.Controller(zeroconf_instance=zeroconf_instance)
|
||||
hass.data[KNOWN_DEVICES] = {}
|
||||
|
||||
return True
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit[IP]==0.2.45"],
|
||||
"requirements": ["aiohomekit[IP]==0.2.46"],
|
||||
"zeroconf": ["_hap._tcp.local."],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@Jc2k"]
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "IQVIA",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iqvia",
|
||||
"requirements": ["numpy==1.19.0", "pyiqvia==0.2.1"],
|
||||
"requirements": ["numpy==1.19.1", "pyiqvia==0.2.1"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"domain": "opencv",
|
||||
"name": "OpenCV",
|
||||
"documentation": "https://www.home-assistant.io/integrations/opencv",
|
||||
"requirements": ["numpy==1.19.0", "opencv-python-headless==4.2.0.32"],
|
||||
"requirements": ["numpy==1.19.1", "opencv-python-headless==4.3.0.36"],
|
||||
"codeowners": []
|
||||
}
|
||||
|
||||
@@ -9,58 +9,48 @@ from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import CONF_ACCOUNT_ID, DOMAIN
|
||||
from .const import CONF_ACCOUNT_ID, DOMAIN # pylint: disable=unused-import
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class OVOEnergyFlowHandler(ConfigFlow):
|
||||
|
||||
class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a OVO Energy config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize OVO Energy flow."""
|
||||
|
||||
async def _show_setup_form(self, errors=None):
|
||||
"""Show the setup form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initiated by the user."""
|
||||
if user_input is None:
|
||||
return await self._show_setup_form()
|
||||
|
||||
errors = {}
|
||||
|
||||
client = OVOEnergy()
|
||||
|
||||
try:
|
||||
if (
|
||||
await client.authenticate(
|
||||
user_input.get(CONF_USERNAME), user_input.get(CONF_PASSWORD)
|
||||
if user_input is not None:
|
||||
client = OVOEnergy()
|
||||
try:
|
||||
authenticated = await client.authenticate(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
is not True
|
||||
):
|
||||
errors["base"] = "authorization_error"
|
||||
return await self._show_setup_form(errors)
|
||||
except aiohttp.ClientError:
|
||||
errors["base"] = "connection_error"
|
||||
return await self._show_setup_form(errors)
|
||||
except aiohttp.ClientError:
|
||||
errors["base"] = "connection_error"
|
||||
else:
|
||||
if authenticated:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=client.account_id,
|
||||
data={
|
||||
CONF_USERNAME: user_input.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
|
||||
CONF_ACCOUNT_ID: client.account_id,
|
||||
},
|
||||
return self.async_create_entry(
|
||||
title=client.account_id,
|
||||
data={
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_ACCOUNT_ID: client.account_id,
|
||||
},
|
||||
)
|
||||
|
||||
errors["base"] = "authorization_error"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=USER_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
@@ -4,6 +4,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ovo_energy",
|
||||
"requirements": ["ovoenergy==1.1.6"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@timmo001"]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"authorization_error": "Authorization error. Check your credentials.",
|
||||
"connection_error": "Could not connect to OVO Energy."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"config": {
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"authorization_error": "Authorization error. Check your credentials.",
|
||||
"connection_error": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"description": "Set up an OVO Energy instance to access your energy usage.",
|
||||
"title": "Add OVO Energy"
|
||||
}
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"description": "Set up an OVO Energy instance to access your energy usage.",
|
||||
"title": "Add OVO Energy Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"authorization_error": "Authorization error. Check your credentials.",
|
||||
"connection_error": "Could not connect to OVO Energy."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
"config": {
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"authorization_error": "Authorization error. Check your credentials.",
|
||||
"connection_error": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"description": "Set up an OVO Energy instance to access your energy usage.",
|
||||
"title": "Add OVO Energy"
|
||||
}
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"description": "Set up an OVO Energy instance to access your energy usage.",
|
||||
"title": "Add OVO Energy Account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ async def async_setup_entry(hass, config_entry):
|
||||
_verify_domain_control = verify_domain_control(hass, DOMAIN)
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
client = Client(websession)
|
||||
client = Client(session=websession)
|
||||
|
||||
try:
|
||||
await client.load_local(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Config flow to configure the RainMachine component."""
|
||||
from regenmaschine import login
|
||||
from regenmaschine import Client
|
||||
from regenmaschine.errors import RainMachineError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -59,12 +59,12 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = Client(session=websession)
|
||||
|
||||
try:
|
||||
await login(
|
||||
await client.load_local(
|
||||
user_input[CONF_IP_ADDRESS],
|
||||
user_input[CONF_PASSWORD],
|
||||
websession,
|
||||
port=user_input[CONF_PORT],
|
||||
ssl=user_input.get(CONF_SSL, True),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"name": "RainMachine",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
|
||||
"requirements": ["regenmaschine==1.5.1"],
|
||||
"requirements": ["regenmaschine==2.1.0"],
|
||||
"codeowners": ["@bachya"]
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ async def async_setup(hass, config):
|
||||
)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.warning("Timeout call %s", response.url, exc_info=1)
|
||||
_LOGGER.warning("Timeout call %s", request_url, exc_info=1)
|
||||
|
||||
except aiohttp.ClientError:
|
||||
_LOGGER.error("Client error %s", request_url, exc_info=1)
|
||||
|
||||
@@ -259,7 +259,7 @@ class SeventeenTrackPackageSensor(Entity):
|
||||
self._friendly_name if self._friendly_name else self._tracking_number
|
||||
)
|
||||
message = NOTIFICATION_DELIVERED_MESSAGE.format(
|
||||
self._tracking_number, identification
|
||||
identification, self._tracking_number
|
||||
)
|
||||
title = NOTIFICATION_DELIVERED_TITLE.format(identification)
|
||||
notification_id = NOTIFICATION_DELIVERED_TITLE.format(self._tracking_number)
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"name": "Template",
|
||||
"documentation": "https://www.home-assistant.io/integrations/template",
|
||||
"codeowners": ["@PhracturedBlue", "@tetienne"],
|
||||
"quality_scale": "internal"
|
||||
"quality_scale": "internal",
|
||||
"after_dependencies": ["group"]
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from PIL import Image, ImageDraw, UnidentifiedImageError
|
||||
import numpy as np
|
||||
import tensorflow as tf # pylint: disable=import-error
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.image_processing import (
|
||||
@@ -16,16 +18,21 @@ from homeassistant.components.image_processing import (
|
||||
PLATFORM_SCHEMA,
|
||||
ImageProcessingEntity,
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
from homeassistant.core import split_entity_id
|
||||
from homeassistant.helpers import template
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util.pil import draw_box
|
||||
|
||||
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
||||
|
||||
DOMAIN = "tensorflow"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MATCHES = "matches"
|
||||
ATTR_SUMMARY = "summary"
|
||||
ATTR_TOTAL_MATCHES = "total_matches"
|
||||
ATTR_PROCESS_TIME = "process_time"
|
||||
|
||||
CONF_AREA = "area"
|
||||
CONF_BOTTOM = "bottom"
|
||||
@@ -34,6 +41,7 @@ CONF_CATEGORY = "category"
|
||||
CONF_FILE_OUT = "file_out"
|
||||
CONF_GRAPH = "graph"
|
||||
CONF_LABELS = "labels"
|
||||
CONF_LABEL_OFFSET = "label_offset"
|
||||
CONF_LEFT = "left"
|
||||
CONF_MODEL = "model"
|
||||
CONF_MODEL_DIR = "model_dir"
|
||||
@@ -58,12 +66,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]),
|
||||
vol.Required(CONF_MODEL): vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_GRAPH): cv.isfile,
|
||||
vol.Required(CONF_GRAPH): cv.isdir,
|
||||
vol.Optional(CONF_AREA): AREA_SCHEMA,
|
||||
vol.Optional(CONF_CATEGORIES, default=[]): vol.All(
|
||||
cv.ensure_list, [vol.Any(cv.string, CATEGORY_SCHEMA)]
|
||||
),
|
||||
vol.Optional(CONF_LABELS): cv.isfile,
|
||||
vol.Optional(CONF_LABEL_OFFSET, default=1): int,
|
||||
vol.Optional(CONF_MODEL_DIR): cv.isdir,
|
||||
}
|
||||
),
|
||||
@@ -71,17 +80,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def get_model_detection_function(model):
|
||||
"""Get a tf.function for detection."""
|
||||
|
||||
@tf.function
|
||||
def detect_fn(image):
|
||||
"""Detect objects in image."""
|
||||
|
||||
image, shapes = model.preprocess(image)
|
||||
prediction_dict = model.predict(image, shapes)
|
||||
detections = model.postprocess(prediction_dict, shapes)
|
||||
|
||||
return detections
|
||||
|
||||
return detect_fn
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the TensorFlow image processing platform."""
|
||||
model_config = config.get(CONF_MODEL)
|
||||
model_config = config[CONF_MODEL]
|
||||
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
|
||||
labels = model_config.get(CONF_LABELS) or hass.config.path(
|
||||
"tensorflow", "object_detection", "data", "mscoco_label_map.pbtxt"
|
||||
)
|
||||
checkpoint = os.path.join(model_config[CONF_GRAPH], "checkpoint")
|
||||
pipeline_config = os.path.join(model_config[CONF_GRAPH], "pipeline.config")
|
||||
|
||||
# Make sure locations exist
|
||||
if not os.path.isdir(model_dir) or not os.path.exists(labels):
|
||||
_LOGGER.error("Unable to locate tensorflow models or label map")
|
||||
if (
|
||||
not os.path.isdir(model_dir)
|
||||
or not os.path.isdir(checkpoint)
|
||||
or not os.path.exists(pipeline_config)
|
||||
or not os.path.exists(labels)
|
||||
):
|
||||
_LOGGER.error("Unable to locate tensorflow model or label map")
|
||||
return
|
||||
|
||||
# append custom model path to sys.path
|
||||
@@ -89,18 +121,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
try:
|
||||
# Verify that the TensorFlow Object Detection API is pre-installed
|
||||
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
||||
# These imports shouldn't be moved to the top, because they depend on code from the model_dir.
|
||||
# (The model_dir is created during the manual setup process. See integration docs.)
|
||||
import tensorflow as tf # pylint: disable=import-outside-toplevel
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from object_detection.utils import label_map_util
|
||||
from object_detection.utils import config_util, label_map_util
|
||||
from object_detection.builders import model_builder
|
||||
except ImportError:
|
||||
_LOGGER.error(
|
||||
"No TensorFlow Object Detection library found! Install or compile "
|
||||
"for your system following instructions here: "
|
||||
"https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md"
|
||||
"https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/tf2.md#installation"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -113,22 +144,45 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"PIL at reduced resolution"
|
||||
)
|
||||
|
||||
# Set up Tensorflow graph, session, and label map to pass to processor
|
||||
# pylint: disable=no-member
|
||||
detection_graph = tf.Graph()
|
||||
with detection_graph.as_default():
|
||||
od_graph_def = tf.GraphDef()
|
||||
with tf.gfile.GFile(model_config.get(CONF_GRAPH), "rb") as fid:
|
||||
serialized_graph = fid.read()
|
||||
od_graph_def.ParseFromString(serialized_graph)
|
||||
tf.import_graph_def(od_graph_def, name="")
|
||||
hass.data[DOMAIN] = {CONF_MODEL: None}
|
||||
|
||||
session = tf.Session(graph=detection_graph)
|
||||
label_map = label_map_util.load_labelmap(labels)
|
||||
categories = label_map_util.convert_label_map_to_categories(
|
||||
label_map, max_num_classes=90, use_display_name=True
|
||||
def tensorflow_hass_start(_event):
|
||||
"""Set up TensorFlow model on hass start."""
|
||||
start = time.perf_counter()
|
||||
|
||||
# Load pipeline config and build a detection model
|
||||
pipeline_configs = config_util.get_configs_from_pipeline_file(pipeline_config)
|
||||
detection_model = model_builder.build(
|
||||
model_config=pipeline_configs["model"], is_training=False
|
||||
)
|
||||
|
||||
# Restore checkpoint
|
||||
ckpt = tf.compat.v2.train.Checkpoint(model=detection_model)
|
||||
ckpt.restore(os.path.join(checkpoint, "ckpt-0")).expect_partial()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Model checkpoint restore took %d seconds", time.perf_counter() - start
|
||||
)
|
||||
|
||||
model = get_model_detection_function(detection_model)
|
||||
|
||||
# Preload model cache with empty image tensor
|
||||
inp = np.zeros([2160, 3840, 3], dtype=np.uint8)
|
||||
# The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
|
||||
input_tensor = tf.convert_to_tensor(inp, dtype=tf.float32)
|
||||
# The model expects a batch of images, so add an axis with `tf.newaxis`.
|
||||
input_tensor = input_tensor[tf.newaxis, ...]
|
||||
# Run inference
|
||||
model(input_tensor)
|
||||
|
||||
_LOGGER.debug("Model load took %d seconds", time.perf_counter() - start)
|
||||
hass.data[DOMAIN][CONF_MODEL] = model
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, tensorflow_hass_start)
|
||||
|
||||
category_index = label_map_util.create_category_index_from_labelmap(
|
||||
labels, use_display_name=True
|
||||
)
|
||||
category_index = label_map_util.create_category_index(categories)
|
||||
|
||||
entities = []
|
||||
|
||||
@@ -138,8 +192,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
hass,
|
||||
camera[CONF_ENTITY_ID],
|
||||
camera.get(CONF_NAME),
|
||||
session,
|
||||
detection_graph,
|
||||
category_index,
|
||||
config,
|
||||
)
|
||||
@@ -152,14 +204,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
"""Representation of an TensorFlow image processor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
camera_entity,
|
||||
name,
|
||||
session,
|
||||
detection_graph,
|
||||
category_index,
|
||||
config,
|
||||
self, hass, camera_entity, name, category_index, config,
|
||||
):
|
||||
"""Initialize the TensorFlow entity."""
|
||||
model_config = config.get(CONF_MODEL)
|
||||
@@ -169,13 +214,12 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
self._name = name
|
||||
else:
|
||||
self._name = "TensorFlow {}".format(split_entity_id(camera_entity)[1])
|
||||
self._session = session
|
||||
self._graph = detection_graph
|
||||
self._category_index = category_index
|
||||
self._min_confidence = config.get(CONF_CONFIDENCE)
|
||||
self._file_out = config.get(CONF_FILE_OUT)
|
||||
|
||||
# handle categories and specific detection areas
|
||||
self._label_id_offset = model_config.get(CONF_LABEL_OFFSET)
|
||||
categories = model_config.get(CONF_CATEGORIES)
|
||||
self._include_categories = []
|
||||
self._category_areas = {}
|
||||
@@ -212,6 +256,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
self._matches = {}
|
||||
self._total_matches = 0
|
||||
self._last_image = None
|
||||
self._process_time = 0
|
||||
|
||||
@property
|
||||
def camera_entity(self):
|
||||
@@ -237,6 +282,7 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
category: len(values) for category, values in self._matches.items()
|
||||
},
|
||||
ATTR_TOTAL_MATCHES: self._total_matches,
|
||||
ATTR_PROCESS_TIME: self._process_time,
|
||||
}
|
||||
|
||||
def _save_image(self, image, matches, paths):
|
||||
@@ -281,10 +327,16 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
|
||||
def process_image(self, image):
|
||||
"""Process the image."""
|
||||
model = self.hass.data[DOMAIN][CONF_MODEL]
|
||||
if not model:
|
||||
_LOGGER.debug("Model not yet ready.")
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
import cv2 # pylint: disable=import-error, import-outside-toplevel
|
||||
|
||||
# pylint: disable=no-member
|
||||
img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED)
|
||||
inp = img[:, :, [2, 1, 0]] # BGR->RGB
|
||||
inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3)
|
||||
@@ -303,15 +355,15 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
)
|
||||
inp_expanded = np.expand_dims(inp, axis=0)
|
||||
|
||||
image_tensor = self._graph.get_tensor_by_name("image_tensor:0")
|
||||
boxes = self._graph.get_tensor_by_name("detection_boxes:0")
|
||||
scores = self._graph.get_tensor_by_name("detection_scores:0")
|
||||
classes = self._graph.get_tensor_by_name("detection_classes:0")
|
||||
boxes, scores, classes = self._session.run(
|
||||
[boxes, scores, classes], feed_dict={image_tensor: inp_expanded}
|
||||
)
|
||||
boxes, scores, classes = map(np.squeeze, [boxes, scores, classes])
|
||||
classes = classes.astype(int)
|
||||
# The input needs to be a tensor, convert it using `tf.convert_to_tensor`.
|
||||
input_tensor = tf.convert_to_tensor(inp_expanded, dtype=tf.float32)
|
||||
|
||||
detections = model(input_tensor)
|
||||
boxes = detections["detection_boxes"][0].numpy()
|
||||
scores = detections["detection_scores"][0].numpy()
|
||||
classes = (
|
||||
detections["detection_classes"][0].numpy() + self._label_id_offset
|
||||
).astype(int)
|
||||
|
||||
matches = {}
|
||||
total_matches = 0
|
||||
@@ -367,3 +419,4 @@ class TensorFlowImageProcessor(ImageProcessingEntity):
|
||||
|
||||
self._matches = matches
|
||||
self._total_matches = total_matches
|
||||
self._process_time = time.perf_counter() - start
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
"name": "TensorFlow",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tensorflow",
|
||||
"requirements": [
|
||||
"tensorflow==1.13.2",
|
||||
"numpy==1.19.0",
|
||||
"protobuf==3.6.1",
|
||||
"tensorflow==2.2.0",
|
||||
"tf-slim==1.1.0",
|
||||
"tf-models-official==2.2.1",
|
||||
"pycocotools==2.0.1",
|
||||
"numpy==1.19.1",
|
||||
"protobuf==3.12.2",
|
||||
"pillow==7.1.2"
|
||||
],
|
||||
"codeowners": []
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "trend",
|
||||
"name": "Trend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/trend",
|
||||
"requirements": ["numpy==1.19.0"],
|
||||
"requirements": ["numpy==1.19.1"],
|
||||
"codeowners": [],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"codeowners": ["@OnFreund"],
|
||||
"config_flow": true,
|
||||
"zeroconf": ["_Volumio._tcp.local."],
|
||||
"requirements": ["pyvolumio==0.1"]
|
||||
"requirements": ["pyvolumio==0.1.1"]
|
||||
}
|
||||
@@ -75,8 +75,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.interface = user_input[CONF_INTERFACE]
|
||||
|
||||
# allow optional manual setting of host and mac
|
||||
if self.host is None and self.sid is None:
|
||||
if self.host is None:
|
||||
self.host = user_input.get(CONF_HOST)
|
||||
if self.sid is None:
|
||||
mac_address = user_input.get(CONF_MAC)
|
||||
|
||||
# format sid from mac_address
|
||||
@@ -173,7 +174,9 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
unique_id = mac_address
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: self.host})
|
||||
self._abort_if_unique_id_configured(
|
||||
{CONF_HOST: self.host, CONF_MAC: mac_address}
|
||||
)
|
||||
|
||||
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||
self.context.update({"title_placeholders": {"name": self.host}})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 114
|
||||
PATCH_VERSION = "0b1"
|
||||
PATCH_VERSION = "0b3"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 1)
|
||||
|
||||
@@ -23,6 +23,9 @@ if TYPE_CHECKING:
|
||||
|
||||
SLOW_SETUP_WARNING = 10
|
||||
SLOW_SETUP_MAX_WAIT = 60
|
||||
SLOW_ADD_ENTITY_MAX_WAIT = 10 # Per Entity
|
||||
SLOW_ADD_MIN_TIMEOUT = 60
|
||||
|
||||
PLATFORM_NOT_READY_RETRIES = 10
|
||||
DATA_ENTITY_PLATFORM = "entity_platform"
|
||||
PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds
|
||||
@@ -292,7 +295,17 @@ class EntityPlatform:
|
||||
if not tasks:
|
||||
return
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
timeout = max(SLOW_ADD_ENTITY_MAX_WAIT * len(tasks), SLOW_ADD_MIN_TIMEOUT)
|
||||
try:
|
||||
async with self.hass.timeout.async_timeout(timeout, self.domain):
|
||||
await asyncio.gather(*tasks)
|
||||
except asyncio.TimeoutError:
|
||||
self.logger.warning(
|
||||
"Timed out adding entities for domain %s with platform %s after %ds",
|
||||
self.domain,
|
||||
self.platform_name,
|
||||
timeout,
|
||||
)
|
||||
|
||||
if self._async_unsub_polling is not None or not any(
|
||||
entity.should_poll for entity in self.entities.values()
|
||||
|
||||
@@ -13,7 +13,7 @@ defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
emoji==0.5.4
|
||||
hass-nabucasa==0.35.0
|
||||
home-assistant-frontend==20200805.0
|
||||
home-assistant-frontend==20200807.1
|
||||
importlib-metadata==1.6.0;python_version<'3.8'
|
||||
jinja2>=2.11.1
|
||||
netdisco==2.8.1
|
||||
|
||||
@@ -5,7 +5,7 @@ ignore=tests
|
||||
jobs=2
|
||||
load-plugins=pylint_strict_informational
|
||||
persistent=no
|
||||
extension-pkg-whitelist=ciso8601
|
||||
extension-pkg-whitelist=ciso8601,cv2
|
||||
|
||||
[BASIC]
|
||||
good-names=id,i,j,k,ex,Run,_,fp,T,ev
|
||||
|
||||
+18
-9
@@ -173,7 +173,7 @@ aioguardian==1.0.1
|
||||
aioharmony==0.2.6
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit[IP]==0.2.45
|
||||
aiohomekit[IP]==0.2.46
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -491,7 +491,7 @@ distro==1.5.0
|
||||
dlipower==0.7.165
|
||||
|
||||
# homeassistant.components.doorbird
|
||||
doorbirdpy==2.0.8
|
||||
doorbirdpy==2.1.0
|
||||
|
||||
# homeassistant.components.dovado
|
||||
dovado==0.4.1
|
||||
@@ -733,7 +733,7 @@ hole==0.5.1
|
||||
holidays==0.10.3
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200805.0
|
||||
home-assistant-frontend==20200807.1
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -981,7 +981,7 @@ numato-gpio==0.8.0
|
||||
# homeassistant.components.opencv
|
||||
# homeassistant.components.tensorflow
|
||||
# homeassistant.components.trend
|
||||
numpy==1.19.0
|
||||
numpy==1.19.1
|
||||
|
||||
# homeassistant.components.oasa_telematics
|
||||
oasatelematics==0.3
|
||||
@@ -1002,7 +1002,7 @@ onvif-zeep-async==0.4.0
|
||||
open-garage==0.1.4
|
||||
|
||||
# homeassistant.components.opencv
|
||||
# opencv-python-headless==4.2.0.32
|
||||
# opencv-python-headless==4.3.0.36
|
||||
|
||||
# homeassistant.components.openerz
|
||||
openerz-api==0.1.0
|
||||
@@ -1120,7 +1120,7 @@ proliphix==0.4.1
|
||||
prometheus_client==0.7.1
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
protobuf==3.6.1
|
||||
protobuf==3.12.2
|
||||
|
||||
# homeassistant.components.proxmoxve
|
||||
proxmoxer==1.1.1
|
||||
@@ -1261,6 +1261,9 @@ pychromecast==7.2.0
|
||||
# homeassistant.components.cmus
|
||||
pycmus==0.1.1
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
pycocotools==2.0.1
|
||||
|
||||
# homeassistant.components.comfoconnect
|
||||
pycomfoconnect==0.3
|
||||
|
||||
@@ -1828,7 +1831,7 @@ pyvizio==0.1.49
|
||||
pyvlx==0.2.16
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1
|
||||
pyvolumio==0.1.1
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
@@ -1873,7 +1876,7 @@ raspyrfm-client==1.2.8
|
||||
recollect-waste==1.0.1
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==1.5.1
|
||||
regenmaschine==2.1.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.0
|
||||
@@ -2098,7 +2101,7 @@ temescal==0.1
|
||||
temperusb==1.5.3
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
# tensorflow==1.13.2
|
||||
# tensorflow==2.2.0
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.2.12
|
||||
@@ -2106,6 +2109,12 @@ tesla-powerwall==0.2.12
|
||||
# homeassistant.components.tesla
|
||||
teslajsonpy==0.10.1
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
# tf-models-official==2.2.1
|
||||
|
||||
# homeassistant.components.tensorflow
|
||||
tf-slim==1.1.0
|
||||
|
||||
# homeassistant.components.thermoworks_smoke
|
||||
thermoworks_smoke==0.1.8
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ aioguardian==1.0.1
|
||||
aioharmony==0.2.6
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit[IP]==0.2.45
|
||||
aiohomekit[IP]==0.2.46
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -252,7 +252,7 @@ directv==0.3.0
|
||||
distro==1.5.0
|
||||
|
||||
# homeassistant.components.doorbird
|
||||
doorbirdpy==2.0.8
|
||||
doorbirdpy==2.1.0
|
||||
|
||||
# homeassistant.components.dsmr
|
||||
dsmr_parser==0.18
|
||||
@@ -362,7 +362,7 @@ hole==0.5.1
|
||||
holidays==0.10.3
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200805.0
|
||||
home-assistant-frontend==20200807.1
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@@ -461,7 +461,7 @@ numato-gpio==0.8.0
|
||||
# homeassistant.components.opencv
|
||||
# homeassistant.components.tensorflow
|
||||
# homeassistant.components.trend
|
||||
numpy==1.19.0
|
||||
numpy==1.19.1
|
||||
|
||||
# homeassistant.components.google
|
||||
oauth2client==4.0.0
|
||||
@@ -830,7 +830,7 @@ pyvesync==1.1.0
|
||||
pyvizio==0.1.49
|
||||
|
||||
# homeassistant.components.volumio
|
||||
pyvolumio==0.1
|
||||
pyvolumio==0.1.1
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
@@ -842,7 +842,7 @@ pyzerproc==0.2.5
|
||||
rachiopy==0.1.3
|
||||
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==1.5.1
|
||||
regenmaschine==2.1.0
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.0
|
||||
|
||||
@@ -41,6 +41,7 @@ COMMENT_REQUIREMENTS = (
|
||||
"RPi.GPIO",
|
||||
"smbus-cffi",
|
||||
"tensorflow",
|
||||
"tf-models-official",
|
||||
"VL53L1X2",
|
||||
)
|
||||
|
||||
|
||||
@@ -337,8 +337,6 @@ async def test_execute(hass):
|
||||
const.SOURCE_CLOUD,
|
||||
)
|
||||
|
||||
print(result)
|
||||
|
||||
assert result == {
|
||||
"requestId": REQ_ID,
|
||||
"payload": {
|
||||
|
||||
@@ -1433,6 +1433,11 @@ async def test_modes_input_select(hass):
|
||||
assert helpers.get_google_type(input_select.DOMAIN, None) is not None
|
||||
assert trait.ModesTrait.supported(input_select.DOMAIN, None, None)
|
||||
|
||||
trt = trait.ModesTrait(
|
||||
hass, State("input_select.bla", "unavailable"), BASIC_CONFIG,
|
||||
)
|
||||
assert trt.sync_attributes() == {"availableModes": []}
|
||||
|
||||
trt = trait.ModesTrait(
|
||||
hass,
|
||||
State(
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.components.homekit.const import (
|
||||
DEVICE_CLASS_OCCUPANCY,
|
||||
SERV_DOORBELL,
|
||||
SERV_MOTION_SENSOR,
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||
VIDEO_CODEC_COPY,
|
||||
VIDEO_CODEC_H264_OMX,
|
||||
)
|
||||
@@ -653,18 +654,28 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events):
|
||||
|
||||
assert char.value == 0
|
||||
|
||||
service2 = acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
|
||||
assert service2
|
||||
char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT)
|
||||
assert char2
|
||||
|
||||
assert char2.value == 0
|
||||
|
||||
hass.states.async_set(
|
||||
doorbell_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
assert char2.value == 0
|
||||
|
||||
char.set_value(True)
|
||||
char2.set_value(True)
|
||||
hass.states.async_set(
|
||||
doorbell_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: DEVICE_CLASS_OCCUPANCY}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
assert char2.value == 0
|
||||
|
||||
# Ensure we do not throw when the linked
|
||||
# doorbell sensor is removed
|
||||
@@ -673,6 +684,7 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events):
|
||||
await acc.run_handler()
|
||||
await hass.async_block_till_done()
|
||||
assert char.value == 0
|
||||
assert char2.value == 0
|
||||
|
||||
|
||||
async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, events):
|
||||
@@ -703,3 +715,4 @@ async def test_camera_with_a_missing_linked_doorbell_sensor(hass, run_driver, ev
|
||||
assert acc.category == 17 # Camera
|
||||
|
||||
assert not acc.get_service(SERV_DOORBELL)
|
||||
assert not acc.get_service(SERV_STATELESS_PROGRAMMABLE_SWITCH)
|
||||
|
||||
@@ -50,8 +50,7 @@ async def test_invalid_password(hass):
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainmachine.config_flow.login",
|
||||
side_effect=RainMachineError,
|
||||
"regenmaschine.client.Client.load_local", side_effect=RainMachineError,
|
||||
):
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"}
|
||||
@@ -84,7 +83,7 @@ async def test_step_import(hass):
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainmachine.config_flow.login", return_value=True,
|
||||
"regenmaschine.client.Client.load_local", return_value=True,
|
||||
):
|
||||
result = await flow.async_step_import(import_config=conf)
|
||||
|
||||
@@ -115,7 +114,7 @@ async def test_step_user(hass):
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainmachine.config_flow.login", return_value=True,
|
||||
"regenmaschine.client.Client.load_local", return_value=True,
|
||||
):
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"""The test for the Template sensor platform."""
|
||||
from asyncio import Event
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.bootstrap import async_from_config_dict
|
||||
from homeassistant.const import (
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component, setup_component
|
||||
from homeassistant.setup import ATTR_COMPONENT, async_setup_component, setup_component
|
||||
|
||||
from tests.common import assert_setup_component, get_test_home_assistant
|
||||
|
||||
@@ -438,6 +443,45 @@ class TestTemplateSensor:
|
||||
)
|
||||
|
||||
|
||||
async def test_creating_sensor_loads_group(hass):
|
||||
"""Test setting up template sensor loads group component first."""
|
||||
order = []
|
||||
after_dep_event = Event()
|
||||
|
||||
async def async_setup_group(hass, config):
|
||||
# Make sure group takes longer to load, so that it won't
|
||||
# be loaded first by chance
|
||||
await after_dep_event.wait()
|
||||
|
||||
order.append("group")
|
||||
return True
|
||||
|
||||
async def async_setup_template(
|
||||
hass, config, async_add_entities, discovery_info=None
|
||||
):
|
||||
order.append("sensor.template")
|
||||
return True
|
||||
|
||||
async def set_after_dep_event(event):
|
||||
if event.data[ATTR_COMPONENT] == "sensor":
|
||||
after_dep_event.set()
|
||||
|
||||
hass.bus.async_listen(EVENT_COMPONENT_LOADED, set_after_dep_event)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.group.async_setup", new=async_setup_group,
|
||||
), patch(
|
||||
"homeassistant.components.template.sensor.async_setup_platform",
|
||||
new=async_setup_template,
|
||||
):
|
||||
await async_from_config_dict(
|
||||
{"sensor": {"platform": "template", "sensors": {}}, "group": {}}, hass
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert order == ["group", "sensor.template"]
|
||||
|
||||
|
||||
async def test_available_template_with_entities(hass):
|
||||
"""Test availability tempalates with values from other entities."""
|
||||
hass.states.async_set("sensor.availability_sensor", STATE_OFF)
|
||||
|
||||
@@ -931,3 +931,41 @@ async def test_invalid_entity_id(hass):
|
||||
await platform.async_add_entities([entity])
|
||||
assert entity.hass is None
|
||||
assert entity.platform is None
|
||||
|
||||
|
||||
class MockBlockingEntity(MockEntity):
|
||||
"""Class to mock an entity that will block adding entities."""
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Block for a long time."""
|
||||
await asyncio.sleep(1000)
|
||||
|
||||
|
||||
async def test_setup_entry_with_entities_that_block_forever(hass, caplog):
|
||||
"""Test we cancel adding entities when we reach the timeout."""
|
||||
registry = mock_registry(hass)
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Mock setup entry method."""
|
||||
async_add_entities([MockBlockingEntity(name="test1", unique_id="unique")])
|
||||
return True
|
||||
|
||||
platform = MockPlatform(async_setup_entry=async_setup_entry)
|
||||
config_entry = MockConfigEntry(entry_id="super-mock-id")
|
||||
mock_entity_platform = MockEntityPlatform(
|
||||
hass, platform_name=config_entry.domain, platform=platform
|
||||
)
|
||||
|
||||
with patch.object(entity_platform, "SLOW_ADD_ENTITY_MAX_WAIT", 0.01), patch.object(
|
||||
entity_platform, "SLOW_ADD_MIN_TIMEOUT", 0.01
|
||||
):
|
||||
assert await mock_entity_platform.async_setup_entry(config_entry)
|
||||
await hass.async_block_till_done()
|
||||
full_name = f"{mock_entity_platform.domain}.{config_entry.domain}"
|
||||
assert full_name in hass.config.components
|
||||
assert len(hass.states.async_entity_ids()) == 0
|
||||
assert len(registry.entities) == 1
|
||||
assert "Timed out adding entities" in caplog.text
|
||||
assert "test_domain.test1" in caplog.text
|
||||
assert "test_domain" in caplog.text
|
||||
assert "test" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user