Compare commits

..

23 Commits

Author SHA1 Message Date
Paulus Schoutsen c0be9aca48 Bumped version to 0.114.0b3 2020-08-09 11:30:45 +00:00
On Freund cb51a00c37 Bump pyvolumio to 0.1.1 (#38685) 2020-08-09 11:30:38 +00:00
Aaron Bach 6f5884805e Fix missing data for Guardian "AP enabled" binary sensor (#38681) 2020-08-09 11:30:37 +00:00
J. Nick Koston 65450d8518 Update aiohomekit to handle homekit devices that do not send format (#38679) 2020-08-09 11:30:36 +00:00
J. Nick Koston f1e3023d44 Ensure shared zeroconf is passed to homekit controller devices (#38678) 2020-08-09 11:30:36 +00:00
Alejandro Rivera aa4e879e1a Fix rest_command UnboundLocalError in exception handling (#38656)
```
2020-08-07 22:38:10 ERROR (MainThread) [homeassistant.components.websocket_api.http.connection.3903193064] local variable 'response' referenced before assignment
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/rest_command/__init__.py", line 115, in async_service_handler
    async with getattr(websession, method)(
  File "/usr/local/lib/python3.8/site-packages/aiohttp/client.py", line 1012, in __aenter__
    self._resp = await self._coro
  File "/usr/local/lib/python3.8/site-packages/aiohttp/client.py", line 582, in _request
    break
  File "/usr/local/lib/python3.8/site-packages/aiohttp/helpers.py", line 586, in __exit__
    raise asyncio.TimeoutError from None
asyncio.exceptions.TimeoutError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 125, in handle_call_service
    await hass.services.async_call(
  File "/usr/src/homeassistant/homeassistant/core.py", line 1281, in async_call
    task.result()
  File "/usr/src/homeassistant/homeassistant/core.py", line 1316, in _execute_service
    await handler.func(service_call)
  File "/usr/src/homeassistant/homeassistant/components/rest_command/__init__.py", line 137, in async_service_handler
    _LOGGER.warning("Timeout call %s", response.url, exc_info=1)
UnboundLocalError: local variable 'response' referenced before assignment
```
2020-08-09 11:30:35 +00:00
Chris Talkington 43961dc36b Fix AccuWeather async timeout (#38654) 2020-08-09 11:30:34 +00:00
Aaron Bach 24c3cbfff9 Bump regenmaschine to 2.1.0 (#38649) 2020-08-09 11:30:33 +00:00
Ole-Martin Heggen 95ffe12264 Fix url in seventeentrack delivered notification (#38646) 2020-08-09 11:30:33 +00:00
Franck Nijhof eac5619001 Remove tf-models-official from wheels builder (#38637) 2020-08-09 11:30:32 +00:00
Joakim Sørensen ab9df350fd Update frontend to 20200807.1 (#38626) 2020-08-09 11:30:32 +00:00
starkillerOG a58a67923b Fix xiaomi_aqara discovery (#38622) 2020-08-09 11:30:31 +00:00
Thomas Hollstegge a92bc562d3 Make sure groups are initialized before template sensors (#37766)
* Make sure groups are initialized before template sensors

This way users may use the `expand` function in templates to expand
groups and have HA listen for changes to group members.

Fixes #35872

* Patch async_setup_platform instead of async_setup

* Cleanup

* Use an event to avoid sleep

* Update tests/components/template/test_sensor.py

Co-authored-by: J. Nick Koston <nick@koston.org>

Co-authored-by: J. Nick Koston <nick@koston.org>
2020-08-09 11:30:30 +00:00
Paulus Schoutsen 214bc81d02 Fix lint 2020-08-07 10:02:10 +00:00
Paulus Schoutsen b626368b6a Bumped version to 0.114.0b2 2020-08-07 08:45:05 +00:00
J. Nick Koston 30e1ff83b9 Ensure doorbird does not block startup (#38619) 2020-08-07 08:44:53 +00:00
Austin Drummond 6e31a2e67d Expose video doorbell button state to HomeKit (#38617) 2020-08-07 08:44:52 +00:00
Pascal Vizeli 64749a0f85 Bump OpenCV 4.3.0 and Numpy 1.19.1 (#38616) 2020-08-07 08:44:31 +00:00
Paulus Schoutsen 6baded622b Handle unavailable input_select in Google Assistant (#38611) 2020-08-07 08:44:07 +00:00
J. Nick Koston 790a136c0a Ensure homekit pairing barcode is usable on dark themes (#38609) 2020-08-07 08:44:07 +00:00
Pascal Vizeli 4fc56cec1c V2 timeout for async_add_entities (#38601)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-08-07 08:44:06 +00:00
Aidan Timson 93cdd4dbf3 Improve the OVO Energy integration (#38598)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2020-08-07 08:43:41 +00:00
Jason Hunter f0f112ff42 Upgrade to TensorFlow 2 (#38384)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2020-08-07 08:42:37 +00:00
40 changed files with 387 additions and 199 deletions
@@ -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,
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -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": []
+1 -1
View File
@@ -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 -1
View File
@@ -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)
+14 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
+6 -6
View File
@@ -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
+1
View File
@@ -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)
+45 -1
View File
@@ -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)
+38
View File
@@ -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