mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Merge pull request #36155 from home-assistant/rc
This commit is contained in:
commit
4054b1744f
@ -38,7 +38,7 @@ from .const import (
|
|||||||
PLATFORMS = ["air_quality", "sensor"]
|
PLATFORMS = ["air_quality", "sensor"]
|
||||||
|
|
||||||
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
|
||||||
DEFAULT_NODE_PRO_SCAN_INTERVAL = timedelta(minutes=1)
|
DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1)
|
||||||
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}
|
DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True}
|
||||||
|
|
||||||
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
|
GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema(
|
||||||
@ -95,27 +95,43 @@ def async_get_cloud_api_update_interval(hass, api_key):
|
|||||||
This will shift based on the number of active consumers, thus keeping the user
|
This will shift based on the number of active consumers, thus keeping the user
|
||||||
under the monthly API limit.
|
under the monthly API limit.
|
||||||
"""
|
"""
|
||||||
num_consumers = len(
|
num_consumers = len(async_get_cloud_coordinators_by_api_key(hass, api_key))
|
||||||
{
|
|
||||||
config_entry
|
|
||||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
|
||||||
if config_entry.data.get(CONF_API_KEY) == api_key
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note
|
# Assuming 10,000 calls per month and a "smallest possible month" of 28 days; note
|
||||||
# that we give a buffer of 1500 API calls for any drift, restarts, etc.:
|
# that we give a buffer of 1500 API calls for any drift, restarts, etc.:
|
||||||
minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers))
|
minutes_between_api_calls = ceil(1 / (8500 / 28 / 24 / 60 / num_consumers))
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Leveling API key usage (%s): %s consumers, %s minutes between updates",
|
||||||
|
api_key,
|
||||||
|
num_consumers,
|
||||||
|
minutes_between_api_calls,
|
||||||
|
)
|
||||||
|
|
||||||
return timedelta(minutes=minutes_between_api_calls)
|
return timedelta(minutes=minutes_between_api_calls)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_reset_coordinator_update_intervals(hass, update_interval):
|
def async_get_cloud_coordinators_by_api_key(hass, api_key):
|
||||||
"""Update any existing data coordinators with a new update interval."""
|
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||||
if not hass.data[DOMAIN][DATA_COORDINATOR]:
|
coordinators = []
|
||||||
return
|
for entry_id, coordinator in hass.data[DOMAIN][DATA_COORDINATOR].items():
|
||||||
|
config_entry = hass.config_entries.async_get_entry(entry_id)
|
||||||
|
if config_entry.data.get(CONF_API_KEY) == api_key:
|
||||||
|
coordinators.append(coordinator)
|
||||||
|
return coordinators
|
||||||
|
|
||||||
for coordinator in hass.data[DOMAIN][DATA_COORDINATOR].values():
|
|
||||||
|
@callback
|
||||||
|
def async_sync_geo_coordinator_update_intervals(hass, api_key):
|
||||||
|
"""Sync the update interval for geography-based data coordinators (by API key)."""
|
||||||
|
update_interval = async_get_cloud_api_update_interval(hass, api_key)
|
||||||
|
for coordinator in async_get_cloud_coordinators_by_api_key(hass, api_key):
|
||||||
|
LOGGER.debug(
|
||||||
|
"Updating interval for coordinator: %s, %s",
|
||||||
|
coordinator.name,
|
||||||
|
update_interval,
|
||||||
|
)
|
||||||
coordinator.update_interval = update_interval
|
coordinator.update_interval = update_interval
|
||||||
|
|
||||||
|
|
||||||
@ -194,10 +210,6 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
|
|
||||||
client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
|
client = Client(api_key=config_entry.data[CONF_API_KEY], session=websession)
|
||||||
|
|
||||||
update_interval = async_get_cloud_api_update_interval(
|
|
||||||
hass, config_entry.data[CONF_API_KEY]
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_update_data():
|
async def async_update_data():
|
||||||
"""Get new data from the API."""
|
"""Get new data from the API."""
|
||||||
if CONF_CITY in config_entry.data:
|
if CONF_CITY in config_entry.data:
|
||||||
@ -219,14 +231,19 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
coordinator = DataUpdateCoordinator(
|
coordinator = DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
name="geography data",
|
name=async_get_geography_id(config_entry.data),
|
||||||
update_interval=update_interval,
|
# We give a placeholder update interval in order to create the coordinator;
|
||||||
|
# then, below, we use the coordinator's presence (along with any other
|
||||||
|
# coordinators using the same API key) to calculate an actual, leveled
|
||||||
|
# update interval:
|
||||||
|
update_interval=timedelta(minutes=5),
|
||||||
update_method=async_update_data,
|
update_method=async_update_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure any other, existing config entries that use this API key are updated
|
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||||
# with the new scan interval:
|
async_sync_geo_coordinator_update_intervals(
|
||||||
async_reset_coordinator_update_intervals(hass, update_interval)
|
hass, config_entry.data[CONF_API_KEY]
|
||||||
|
)
|
||||||
|
|
||||||
# Only geography-based entries have options:
|
# Only geography-based entries have options:
|
||||||
config_entry.add_update_listener(async_update_options)
|
config_entry.add_update_listener(async_update_options)
|
||||||
@ -251,13 +268,13 @@ async def async_setup_entry(hass, config_entry):
|
|||||||
hass,
|
hass,
|
||||||
LOGGER,
|
LOGGER,
|
||||||
name="Node/Pro data",
|
name="Node/Pro data",
|
||||||
update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL,
|
update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
|
||||||
update_method=async_update_data,
|
update_method=async_update_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator.async_refresh()
|
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||||
|
|
||||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
for component in PLATFORMS:
|
for component in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
@ -317,6 +334,12 @@ async def async_unload_entry(hass, config_entry):
|
|||||||
)
|
)
|
||||||
if unload_ok:
|
if unload_ok:
|
||||||
hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
|
hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id)
|
||||||
|
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||||
|
# Re-calculate the update interval period for any remaining consumes of this
|
||||||
|
# API key:
|
||||||
|
async_sync_geo_coordinator_update_intervals(
|
||||||
|
hass, config_entry.data[CONF_API_KEY]
|
||||||
|
)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from .const import (
|
|||||||
CONF_INTEGRATION_TYPE,
|
CONF_INTEGRATION_TYPE,
|
||||||
DATA_COORDINATOR,
|
DATA_COORDINATOR,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
INTEGRATION_TYPE_GEOGRAPHY,
|
INTEGRATION_TYPE_NODE_PRO,
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTR_HUMIDITY = "humidity"
|
ATTR_HUMIDITY = "humidity"
|
||||||
@ -18,12 +18,12 @@ ATTR_VOC = "voc"
|
|||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up AirVisual air quality entities based on a config entry."""
|
"""Set up AirVisual air quality entities based on a config entry."""
|
||||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
|
||||||
|
|
||||||
# Geography-based AirVisual integrations don't utilize this platform:
|
# Geography-based AirVisual integrations don't utilize this platform:
|
||||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||||
|
|
||||||
async_add_entities([AirVisualNodeProSensor(coordinator)], True)
|
async_add_entities([AirVisualNodeProSensor(coordinator)], True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,8 +33,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri):
|
|||||||
# Whitelist the iOS and Android callbacks so that people can link apps
|
# Whitelist the iOS and Android callbacks so that people can link apps
|
||||||
# without being connected to the internet.
|
# without being connected to the internet.
|
||||||
if redirect_uri == "homeassistant://auth-callback" and client_id in (
|
if redirect_uri == "homeassistant://auth-callback" and client_id in (
|
||||||
"https://www.home-assistant.io/android",
|
"https://home-assistant.io/android",
|
||||||
"https://www.home-assistant.io/iOS",
|
"https://home-assistant.io/iOS",
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
4
homeassistant/components/emulated_hue/const.py
Normal file
4
homeassistant/components/emulated_hue/const.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Constants for emulated_hue."""
|
||||||
|
|
||||||
|
HUE_SERIAL_NUMBER = "001788FFFE23BFC2"
|
||||||
|
HUE_UUID = "2f402f80-da50-11e1-9b23-001788255acc"
|
@ -9,6 +9,8 @@ from aiohttp import web
|
|||||||
from homeassistant import core
|
from homeassistant import core
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
|
||||||
|
from .const import HUE_SERIAL_NUMBER, HUE_UUID
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -42,8 +44,8 @@ class DescriptionXmlView(HomeAssistantView):
|
|||||||
<modelName>Philips hue bridge 2015</modelName>
|
<modelName>Philips hue bridge 2015</modelName>
|
||||||
<modelNumber>BSB002</modelNumber>
|
<modelNumber>BSB002</modelNumber>
|
||||||
<modelURL>http://www.meethue.com</modelURL>
|
<modelURL>http://www.meethue.com</modelURL>
|
||||||
<serialNumber>001788FFFE23BFC2</serialNumber>
|
<serialNumber>{HUE_SERIAL_NUMBER}</serialNumber>
|
||||||
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
|
<UDN>uuid:{HUE_UUID}</UDN>
|
||||||
</device>
|
</device>
|
||||||
</root>
|
</root>
|
||||||
"""
|
"""
|
||||||
@ -70,21 +72,8 @@ class UPNPResponderThread(threading.Thread):
|
|||||||
self.host_ip_addr = host_ip_addr
|
self.host_ip_addr = host_ip_addr
|
||||||
self.listen_port = listen_port
|
self.listen_port = listen_port
|
||||||
self.upnp_bind_multicast = upnp_bind_multicast
|
self.upnp_bind_multicast = upnp_bind_multicast
|
||||||
|
self.advertise_ip = advertise_ip
|
||||||
# Note that the double newline at the end of
|
self.advertise_port = advertise_port
|
||||||
# this string is required per the SSDP spec
|
|
||||||
resp_template = f"""HTTP/1.1 200 OK
|
|
||||||
CACHE-CONTROL: max-age=60
|
|
||||||
EXT:
|
|
||||||
LOCATION: http://{advertise_ip}:{advertise_port}/description.xml
|
|
||||||
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
|
|
||||||
hue-bridgeid: 001788FFFE23BFC2
|
|
||||||
ST: upnp:rootdevice
|
|
||||||
USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.upnp_response = resp_template.replace("\n", "\r\n").encode("utf-8")
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the server."""
|
"""Run the server."""
|
||||||
@ -136,10 +125,13 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if "M-SEARCH" in data.decode("utf-8", errors="ignore"):
|
if "M-SEARCH" in data.decode("utf-8", errors="ignore"):
|
||||||
|
_LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data)
|
||||||
# SSDP M-SEARCH method received, respond to it with our info
|
# SSDP M-SEARCH method received, respond to it with our info
|
||||||
resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
response = self._handle_request(data)
|
||||||
|
|
||||||
resp_socket.sendto(self.upnp_response, addr)
|
resp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
resp_socket.sendto(response, addr)
|
||||||
|
_LOGGER.debug("UPNP Responder responding with: %s", response)
|
||||||
resp_socket.close()
|
resp_socket.close()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@ -148,6 +140,31 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
|
|||||||
self._interrupted = True
|
self._interrupted = True
|
||||||
self.join()
|
self.join()
|
||||||
|
|
||||||
|
def _handle_request(self, data):
|
||||||
|
if "upnp:rootdevice" in data.decode("utf-8", errors="ignore"):
|
||||||
|
return self._prepare_response(
|
||||||
|
"upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._prepare_response(
|
||||||
|
"urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prepare_response(self, search_target, unique_service_name):
|
||||||
|
# Note that the double newline at the end of
|
||||||
|
# this string is required per the SSDP spec
|
||||||
|
response = f"""HTTP/1.1 200 OK
|
||||||
|
CACHE-CONTROL: max-age=60
|
||||||
|
EXT:
|
||||||
|
LOCATION: http://{self.advertise_ip}:{self.advertise_port}/description.xml
|
||||||
|
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
|
||||||
|
hue-bridgeid: {HUE_SERIAL_NUMBER}
|
||||||
|
ST: {search_target}
|
||||||
|
USN: {unique_service_name}
|
||||||
|
|
||||||
|
"""
|
||||||
|
return response.replace("\n", "\r\n").encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def clean_socket_close(sock):
|
def clean_socket_close(sock):
|
||||||
"""Close a socket connection and logs its closure."""
|
"""Close a socket connection and logs its closure."""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "frontend",
|
"domain": "frontend",
|
||||||
"name": "Home Assistant Frontend",
|
"name": "Home Assistant Frontend",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"requirements": ["home-assistant-frontend==20200519.4"],
|
"requirements": ["home-assistant-frontend==20200519.5"],
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"api",
|
"api",
|
||||||
"auth",
|
"auth",
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -49,6 +50,7 @@ from .const import (
|
|||||||
ATTR_SOFTWARE_VERSION,
|
ATTR_SOFTWARE_VERSION,
|
||||||
ATTR_VALUE,
|
ATTR_VALUE,
|
||||||
BRIDGE_NAME,
|
BRIDGE_NAME,
|
||||||
|
BRIDGE_SERIAL_NUMBER,
|
||||||
CONF_ADVERTISE_IP,
|
CONF_ADVERTISE_IP,
|
||||||
CONF_AUTO_START,
|
CONF_AUTO_START,
|
||||||
CONF_ENTITY_CONFIG,
|
CONF_ENTITY_CONFIG,
|
||||||
@ -434,6 +436,13 @@ class HomeKit:
|
|||||||
interface_choice=self._interface_choice,
|
interface_choice=self._interface_choice,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If we do not load the mac address will be wrong
|
||||||
|
# as pyhap uses a random one until state is restored
|
||||||
|
if os.path.exists(persist_file):
|
||||||
|
self.driver.load()
|
||||||
|
else:
|
||||||
|
self.driver.persist()
|
||||||
|
|
||||||
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
||||||
if self._safe_mode:
|
if self._safe_mode:
|
||||||
_LOGGER.debug("Safe_mode selected for %s", self._name)
|
_LOGGER.debug("Safe_mode selected for %s", self._name)
|
||||||
@ -540,16 +549,45 @@ class HomeKit:
|
|||||||
@callback
|
@callback
|
||||||
def _async_register_bridge(self, dev_reg):
|
def _async_register_bridge(self, dev_reg):
|
||||||
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
|
"""Register the bridge as a device so homekit_controller and exclude it from discovery."""
|
||||||
|
formatted_mac = device_registry.format_mac(self.driver.state.mac)
|
||||||
|
# Connections and identifiers are both used here.
|
||||||
|
#
|
||||||
|
# connections exists so homekit_controller can know the
|
||||||
|
# virtual mac address of the bridge and know to not offer
|
||||||
|
# it via discovery.
|
||||||
|
#
|
||||||
|
# identifiers is used as well since the virtual mac may change
|
||||||
|
# because it will not survive manual pairing resets (deleting state file)
|
||||||
|
# which we have trained users to do over the past few years
|
||||||
|
# because this was the way you had to fix homekit when pairing
|
||||||
|
# failed.
|
||||||
|
#
|
||||||
|
connection = (device_registry.CONNECTION_NETWORK_MAC, formatted_mac)
|
||||||
|
identifier = (DOMAIN, self._entry_id, BRIDGE_SERIAL_NUMBER)
|
||||||
|
self._async_purge_old_bridges(dev_reg, identifier, connection)
|
||||||
dev_reg.async_get_or_create(
|
dev_reg.async_get_or_create(
|
||||||
config_entry_id=self._entry_id,
|
config_entry_id=self._entry_id,
|
||||||
connections={
|
identifiers={identifier},
|
||||||
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
|
connections={connection},
|
||||||
},
|
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
name=self._name,
|
name=self._name,
|
||||||
model="Home Assistant HomeKit Bridge",
|
model="Home Assistant HomeKit Bridge",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_purge_old_bridges(self, dev_reg, identifier, connection):
|
||||||
|
"""Purge bridges that exist from failed pairing or manual resets."""
|
||||||
|
devices_to_purge = []
|
||||||
|
for entry in dev_reg.devices.values():
|
||||||
|
if self._entry_id in entry.config_entries and (
|
||||||
|
identifier not in entry.identifiers
|
||||||
|
or connection not in entry.connections
|
||||||
|
):
|
||||||
|
devices_to_purge.append(entry.id)
|
||||||
|
|
||||||
|
for device_id in devices_to_purge:
|
||||||
|
dev_reg.async_remove_device(device_id)
|
||||||
|
|
||||||
def _start(self, bridged_states):
|
def _start(self, bridged_states):
|
||||||
from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel
|
from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel
|
||||||
type_cameras,
|
type_cameras,
|
||||||
|
@ -28,6 +28,7 @@ from .const import (
|
|||||||
DEVICE_INFO,
|
DEVICE_INFO,
|
||||||
DEVICE_MODEL,
|
DEVICE_MODEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
LEGACY_DEVICE_MODEL,
|
||||||
PV_API,
|
PV_API,
|
||||||
PV_ROOM_DATA,
|
PV_ROOM_DATA,
|
||||||
PV_SHADE_DATA,
|
PV_SHADE_DATA,
|
||||||
@ -118,7 +119,7 @@ class PowerViewShade(ShadeEntity, CoverEntity):
|
|||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Flag supported features."""
|
"""Flag supported features."""
|
||||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||||
if self._device_info[DEVICE_MODEL] != "1":
|
if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL:
|
||||||
supported_features |= SUPPORT_STOP
|
supported_features |= SUPPORT_STOP
|
||||||
return supported_features
|
return supported_features
|
||||||
|
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
||||||
"codeowners": ["@flz"],
|
"codeowners": ["@flz"],
|
||||||
"requirements": ["iaqualink==0.3.1"]
|
"requirements": ["iaqualink==0.3.3"]
|
||||||
}
|
}
|
||||||
|
@ -34,16 +34,25 @@ class HassAqualinkSensor(AqualinkEntity):
|
|||||||
return self.dev.label
|
return self.dev.label
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self) -> str:
|
def unit_of_measurement(self) -> Optional[str]:
|
||||||
"""Return the measurement unit for the sensor."""
|
"""Return the measurement unit for the sensor."""
|
||||||
if self.dev.system.temp_unit == "F":
|
if self.dev.name.endswith("_temp"):
|
||||||
return TEMP_FAHRENHEIT
|
if self.dev.system.temp_unit == "F":
|
||||||
return TEMP_CELSIUS
|
return TEMP_FAHRENHEIT
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str:
|
def state(self) -> Optional[str]:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return int(self.dev.state) if self.dev.state != "" else None
|
if self.dev.state == "":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = int(self.dev.state)
|
||||||
|
except ValueError:
|
||||||
|
state = float(self.dev.state)
|
||||||
|
return state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self) -> Optional[str]:
|
def device_class(self) -> Optional[str]:
|
||||||
|
@ -103,6 +103,8 @@ class EmailReader:
|
|||||||
|
|
||||||
if message_data is None:
|
if message_data is None:
|
||||||
return None
|
return None
|
||||||
|
if message_data[0] is None:
|
||||||
|
return None
|
||||||
raw_email = message_data[0][1]
|
raw_email = message_data[0][1]
|
||||||
email_message = email.message_from_bytes(raw_email)
|
email_message = email.message_from_bytes(raw_email)
|
||||||
return email_message
|
return email_message
|
||||||
@ -126,13 +128,22 @@ class EmailReader:
|
|||||||
self._last_id = int(message_uid)
|
self._last_id = int(message_uid)
|
||||||
return self._fetch_message(message_uid)
|
return self._fetch_message(message_uid)
|
||||||
|
|
||||||
|
return self._fetch_message(str(self._last_id))
|
||||||
|
|
||||||
except imaplib.IMAP4.error:
|
except imaplib.IMAP4.error:
|
||||||
_LOGGER.info("Connection to %s lost, attempting to reconnect", self._server)
|
_LOGGER.info("Connection to %s lost, attempting to reconnect", self._server)
|
||||||
try:
|
try:
|
||||||
self.connect()
|
self.connect()
|
||||||
|
_LOGGER.info(
|
||||||
|
"Reconnect to %s succeeded, trying last message", self._server
|
||||||
|
)
|
||||||
|
if self._last_id is not None:
|
||||||
|
return self._fetch_message(str(self._last_id))
|
||||||
except imaplib.IMAP4.error:
|
except imaplib.IMAP4.error:
|
||||||
_LOGGER.error("Failed to reconnect")
|
_LOGGER.error("Failed to reconnect")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class EmailContentSensor(Entity):
|
class EmailContentSensor(Entity):
|
||||||
"""Representation of an EMail sensor."""
|
"""Representation of an EMail sensor."""
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .device import ONVIFDevice
|
from .device import ONVIFDevice
|
||||||
from .models import Profile
|
from .models import Profile
|
||||||
|
|
||||||
@ -11,8 +12,8 @@ class ONVIFBaseEntity(Entity):
|
|||||||
|
|
||||||
def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None:
|
def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None:
|
||||||
"""Initialize the ONVIF entity."""
|
"""Initialize the ONVIF entity."""
|
||||||
self.device = device
|
self.device: ONVIFDevice = device
|
||||||
self.profile = profile
|
self.profile: Profile = profile
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self):
|
||||||
@ -22,10 +23,25 @@ class ONVIFBaseEntity(Entity):
|
|||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
return {
|
device_info = {
|
||||||
"connections": {(CONNECTION_NETWORK_MAC, self.device.info.mac)},
|
|
||||||
"manufacturer": self.device.info.manufacturer,
|
"manufacturer": self.device.info.manufacturer,
|
||||||
"model": self.device.info.model,
|
"model": self.device.info.model,
|
||||||
"name": self.device.name,
|
"name": self.device.name,
|
||||||
"sw_version": self.device.info.fw_version,
|
"sw_version": self.device.info.fw_version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# MAC address is not always available, and given the number
|
||||||
|
# of non-conformant ONVIF devices we have historically supported,
|
||||||
|
# we can not guarantee serial number either. Due to this, we have
|
||||||
|
# adopted an either/or approach in the config entry setup, and can
|
||||||
|
# guarantee that one or the other will be populated.
|
||||||
|
# See: https://github.com/home-assistant/core/issues/35883
|
||||||
|
if self.device.info.serial_number:
|
||||||
|
device_info["identifiers"] = {(DOMAIN, self.device.info.serial_number)}
|
||||||
|
|
||||||
|
if self.device.info.mac:
|
||||||
|
device_info["connections"] = {
|
||||||
|
(CONNECTION_NETWORK_MAC, self.device.info.mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
return device_info
|
||||||
|
@ -96,8 +96,8 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera):
|
|||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
"""Return a unique ID."""
|
"""Return a unique ID."""
|
||||||
if self.profile.index:
|
if self.profile.index:
|
||||||
return f"{self.device.info.mac}_{self.profile.index}"
|
return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}"
|
||||||
return self.device.info.mac
|
return self.device.info.mac or self.device.info.serial_number
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_registry_enabled_default(self) -> bool:
|
def entity_registry_enabled_default(self) -> bool:
|
||||||
|
@ -169,10 +169,16 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
self.onvif_config[CONF_PASSWORD] = user_input[CONF_PASSWORD]
|
||||||
return await self.async_step_profiles()
|
return await self.async_step_profiles()
|
||||||
|
|
||||||
|
# Password is optional and default empty due to some cameras not
|
||||||
|
# allowing you to change ONVIF user settings.
|
||||||
|
# See https://github.com/home-assistant/core/issues/35904
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="auth",
|
step_id="auth",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
{
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Optional(CONF_PASSWORD, default=""): str,
|
||||||
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -195,15 +201,21 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
await device.update_xaddrs()
|
await device.update_xaddrs()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
device_mgmt = device.create_devicemgmt_service()
|
||||||
|
|
||||||
# Get the MAC address to use as the unique ID for the config flow
|
# Get the MAC address to use as the unique ID for the config flow
|
||||||
if not self.device_id:
|
if not self.device_id:
|
||||||
devicemgmt = device.create_devicemgmt_service()
|
network_interfaces = await device_mgmt.GetNetworkInterfaces()
|
||||||
network_interfaces = await devicemgmt.GetNetworkInterfaces()
|
|
||||||
for interface in network_interfaces:
|
for interface in network_interfaces:
|
||||||
if interface.Enabled:
|
if interface.Enabled:
|
||||||
self.device_id = interface.Info.HwAddress
|
self.device_id = interface.Info.HwAddress
|
||||||
|
|
||||||
if self.device_id is None:
|
# If no network interfaces are exposed, fallback to serial number
|
||||||
|
if not self.device_id:
|
||||||
|
device_info = await device_mgmt.GetDeviceInformation()
|
||||||
|
self.device_id = device_info.SerialNumber
|
||||||
|
|
||||||
|
if not self.device_id:
|
||||||
return self.async_abort(reason="no_mac")
|
return self.async_abort(reason="no_mac")
|
||||||
|
|
||||||
await self.async_set_unique_id(self.device_id, raise_on_progress=False)
|
await self.async_set_unique_id(self.device_id, raise_on_progress=False)
|
||||||
|
@ -148,12 +148,12 @@ class ONVIFDevice:
|
|||||||
async def async_check_date_and_time(self) -> None:
|
async def async_check_date_and_time(self) -> None:
|
||||||
"""Warns if device and system date not synced."""
|
"""Warns if device and system date not synced."""
|
||||||
LOGGER.debug("Setting up the ONVIF device management service")
|
LOGGER.debug("Setting up the ONVIF device management service")
|
||||||
devicemgmt = self.device.create_devicemgmt_service()
|
device_mgmt = self.device.create_devicemgmt_service()
|
||||||
|
|
||||||
LOGGER.debug("Retrieving current device date/time")
|
LOGGER.debug("Retrieving current device date/time")
|
||||||
try:
|
try:
|
||||||
system_date = dt_util.utcnow()
|
system_date = dt_util.utcnow()
|
||||||
device_time = await devicemgmt.GetSystemDateAndTime()
|
device_time = await device_mgmt.GetSystemDateAndTime()
|
||||||
if not device_time:
|
if not device_time:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"""Couldn't get device '%s' date/time.
|
"""Couldn't get device '%s' date/time.
|
||||||
@ -212,13 +212,22 @@ class ONVIFDevice:
|
|||||||
|
|
||||||
async def async_get_device_info(self) -> DeviceInfo:
|
async def async_get_device_info(self) -> DeviceInfo:
|
||||||
"""Obtain information about this device."""
|
"""Obtain information about this device."""
|
||||||
devicemgmt = self.device.create_devicemgmt_service()
|
device_mgmt = self.device.create_devicemgmt_service()
|
||||||
device_info = await devicemgmt.GetDeviceInformation()
|
device_info = await device_mgmt.GetDeviceInformation()
|
||||||
|
|
||||||
|
# Grab the last MAC address for backwards compatibility
|
||||||
|
mac = None
|
||||||
|
network_interfaces = await device_mgmt.GetNetworkInterfaces()
|
||||||
|
for interface in network_interfaces:
|
||||||
|
if interface.Enabled:
|
||||||
|
mac = interface.Info.HwAddress
|
||||||
|
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
device_info.Manufacturer,
|
device_info.Manufacturer,
|
||||||
device_info.Model,
|
device_info.Model,
|
||||||
device_info.FirmwareVersion,
|
device_info.FirmwareVersion,
|
||||||
self.config_entry.unique_id,
|
device_info.SerialNumber,
|
||||||
|
mac,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_get_capabilities(self):
|
async def async_get_capabilities(self):
|
||||||
@ -228,7 +237,7 @@ class ONVIFDevice:
|
|||||||
media_service = self.device.create_media_service()
|
media_service = self.device.create_media_service()
|
||||||
media_capabilities = await media_service.GetServiceCapabilities()
|
media_capabilities = await media_service.GetServiceCapabilities()
|
||||||
snapshot = media_capabilities and media_capabilities.SnapshotUri
|
snapshot = media_capabilities and media_capabilities.SnapshotUri
|
||||||
except (ONVIFError, Fault):
|
except (ONVIFError, Fault, ServerDisconnectedError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
pullpoint = False
|
pullpoint = False
|
||||||
@ -415,7 +424,7 @@ class ONVIFDevice:
|
|||||||
"PTZ preset '%s' does not exist on device '%s'. Available Presets: %s",
|
"PTZ preset '%s' does not exist on device '%s'. Available Presets: %s",
|
||||||
preset_val,
|
preset_val,
|
||||||
self.name,
|
self.name,
|
||||||
profile.ptz.presets.join(", "),
|
", ".join(profile.ptz.presets),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -62,7 +62,8 @@ class EventManager:
|
|||||||
@callback
|
@callback
|
||||||
def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
||||||
"""Remove data update."""
|
"""Remove data update."""
|
||||||
self._listeners.remove(update_callback)
|
if update_callback in self._listeners:
|
||||||
|
self._listeners.remove(update_callback)
|
||||||
|
|
||||||
if not self._listeners and self._unsub_refresh:
|
if not self._listeners and self._unsub_refresh:
|
||||||
self._unsub_refresh()
|
self._unsub_refresh()
|
||||||
@ -93,6 +94,8 @@ class EventManager:
|
|||||||
|
|
||||||
async def async_stop(self) -> None:
|
async def async_stop(self) -> None:
|
||||||
"""Unsubscribe from events."""
|
"""Unsubscribe from events."""
|
||||||
|
self._listeners = []
|
||||||
|
|
||||||
if not self._subscription:
|
if not self._subscription:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -144,6 +147,10 @@ class EventManager:
|
|||||||
async def async_parse_messages(self, messages) -> None:
|
async def async_parse_messages(self, messages) -> None:
|
||||||
"""Parse notification message."""
|
"""Parse notification message."""
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
|
# Guard against empty message
|
||||||
|
if not msg.Topic:
|
||||||
|
continue
|
||||||
|
|
||||||
topic = msg.Topic._value_1
|
topic = msg.Topic._value_1
|
||||||
parser = PARSERS.get(topic)
|
parser = PARSERS.get(topic)
|
||||||
if not parser:
|
if not parser:
|
||||||
|
@ -10,6 +10,7 @@ class DeviceInfo:
|
|||||||
manufacturer: str = None
|
manufacturer: str = None
|
||||||
model: str = None
|
model: str = None
|
||||||
fw_version: str = None
|
fw_version: str = None
|
||||||
|
serial_number: str = None
|
||||||
mac: str = None
|
mac: str = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
"""Set up the OpenGarage covers."""
|
"""Set up the OpenGarage covers."""
|
||||||
covers = []
|
covers = []
|
||||||
devices = config.get(CONF_COVERS)
|
devices = config.get(CONF_COVERS)
|
||||||
@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||||||
|
|
||||||
covers.append(OpenGarageCover(device_config.get(CONF_NAME), open_garage))
|
covers.append(OpenGarageCover(device_config.get(CONF_NAME), open_garage))
|
||||||
|
|
||||||
add_entities(covers, True)
|
async_add_entities(covers, True)
|
||||||
|
|
||||||
|
|
||||||
class OpenGarageCover(CoverEntity):
|
class OpenGarageCover(CoverEntity):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "roku",
|
"domain": "roku",
|
||||||
"name": "Roku",
|
"name": "Roku",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||||
"requirements": ["rokuecp==0.4.1"],
|
"requirements": ["rokuecp==0.4.2"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "roku:ecp",
|
"st": "roku:ecp",
|
||||||
|
@ -171,15 +171,18 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
|||||||
|
|
||||||
async def async_media_pause(self) -> None:
|
async def async_media_pause(self) -> None:
|
||||||
"""Send pause command."""
|
"""Send pause command."""
|
||||||
await self.coordinator.roku.remote("play")
|
if self.state != STATE_STANDBY:
|
||||||
|
await self.coordinator.roku.remote("play")
|
||||||
|
|
||||||
async def async_media_play(self) -> None:
|
async def async_media_play(self) -> None:
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
await self.coordinator.roku.remote("play")
|
if self.state != STATE_STANDBY:
|
||||||
|
await self.coordinator.roku.remote("play")
|
||||||
|
|
||||||
async def async_media_play_pause(self) -> None:
|
async def async_media_play_pause(self) -> None:
|
||||||
"""Send play/pause command."""
|
"""Send play/pause command."""
|
||||||
await self.coordinator.roku.remote("play")
|
if self.state != STATE_STANDBY:
|
||||||
|
await self.coordinator.roku.remote("play")
|
||||||
|
|
||||||
async def async_media_previous_track(self) -> None:
|
async def async_media_previous_track(self) -> None:
|
||||||
"""Send previous track command."""
|
"""Send previous track command."""
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
"domain": "velux",
|
"domain": "velux",
|
||||||
"name": "Velux",
|
"name": "Velux",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/velux",
|
"documentation": "https://www.home-assistant.io/integrations/velux",
|
||||||
"requirements": ["pyvlx==0.2.14"],
|
"requirements": ["pyvlx==0.2.16"],
|
||||||
"codeowners": ["@Julius2342"]
|
"codeowners": ["@Julius2342"]
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""Constants used by Home Assistant components."""
|
"""Constants used by Home Assistant components."""
|
||||||
MAJOR_VERSION = 0
|
MAJOR_VERSION = 0
|
||||||
MINOR_VERSION = 110
|
MINOR_VERSION = 110
|
||||||
PATCH_VERSION = "2"
|
PATCH_VERSION = "3"
|
||||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||||
|
@ -12,7 +12,7 @@ cryptography==2.9.2
|
|||||||
defusedxml==0.6.0
|
defusedxml==0.6.0
|
||||||
distro==1.5.0
|
distro==1.5.0
|
||||||
hass-nabucasa==0.34.3
|
hass-nabucasa==0.34.3
|
||||||
home-assistant-frontend==20200519.4
|
home-assistant-frontend==20200519.5
|
||||||
importlib-metadata==1.6.0
|
importlib-metadata==1.6.0
|
||||||
jinja2>=2.11.1
|
jinja2>=2.11.1
|
||||||
netdisco==2.6.0
|
netdisco==2.6.0
|
||||||
|
@ -731,7 +731,7 @@ hole==0.5.1
|
|||||||
holidays==0.10.2
|
holidays==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20200519.4
|
home-assistant-frontend==20200519.5
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -764,7 +764,7 @@ hydrawiser==0.1.1
|
|||||||
iammeter==0.1.7
|
iammeter==0.1.7
|
||||||
|
|
||||||
# homeassistant.components.iaqualink
|
# homeassistant.components.iaqualink
|
||||||
iaqualink==0.3.1
|
iaqualink==0.3.3
|
||||||
|
|
||||||
# homeassistant.components.watson_tts
|
# homeassistant.components.watson_tts
|
||||||
ibm-watson==4.0.1
|
ibm-watson==4.0.1
|
||||||
@ -1802,7 +1802,7 @@ pyvesync==1.1.0
|
|||||||
pyvizio==0.1.47
|
pyvizio==0.1.47
|
||||||
|
|
||||||
# homeassistant.components.velux
|
# homeassistant.components.velux
|
||||||
pyvlx==0.2.14
|
pyvlx==0.2.16
|
||||||
|
|
||||||
# homeassistant.components.html5
|
# homeassistant.components.html5
|
||||||
pywebpush==1.9.2
|
pywebpush==1.9.2
|
||||||
@ -1871,7 +1871,7 @@ rjpl==0.3.5
|
|||||||
rocketchat-API==0.6.1
|
rocketchat-API==0.6.1
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.4.1
|
rokuecp==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.1
|
roombapy==1.6.1
|
||||||
|
@ -312,7 +312,7 @@ hole==0.5.1
|
|||||||
holidays==0.10.2
|
holidays==0.10.2
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20200519.4
|
home-assistant-frontend==20200519.5
|
||||||
|
|
||||||
# homeassistant.components.zwave
|
# homeassistant.components.zwave
|
||||||
homeassistant-pyozw==0.1.10
|
homeassistant-pyozw==0.1.10
|
||||||
@ -331,7 +331,7 @@ httplib2==0.10.3
|
|||||||
huawei-lte-api==1.4.12
|
huawei-lte-api==1.4.12
|
||||||
|
|
||||||
# homeassistant.components.iaqualink
|
# homeassistant.components.iaqualink
|
||||||
iaqualink==0.3.1
|
iaqualink==0.3.3
|
||||||
|
|
||||||
# homeassistant.components.influxdb
|
# homeassistant.components.influxdb
|
||||||
influxdb==5.2.3
|
influxdb==5.2.3
|
||||||
@ -762,7 +762,7 @@ rflink==0.0.52
|
|||||||
ring_doorbell==0.6.0
|
ring_doorbell==0.6.0
|
||||||
|
|
||||||
# homeassistant.components.roku
|
# homeassistant.components.roku
|
||||||
rokuecp==0.4.1
|
rokuecp==0.4.2
|
||||||
|
|
||||||
# homeassistant.components.roomba
|
# homeassistant.components.roomba
|
||||||
roombapy==1.6.1
|
roombapy==1.6.1
|
||||||
|
@ -166,8 +166,7 @@ async def test_find_link_tag_max_size(hass, mock_session):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"client_id",
|
"client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"],
|
||||||
["https://www.home-assistant.io/android", "https://www.home-assistant.io/iOS"],
|
|
||||||
)
|
)
|
||||||
async def test_verify_redirect_uri_android_ios(client_id):
|
async def test_verify_redirect_uri_android_ios(client_id):
|
||||||
"""Test that we verify redirect uri correctly for Android/iOS."""
|
"""Test that we verify redirect uri correctly for Android/iOS."""
|
||||||
|
@ -48,6 +48,66 @@ class TestEmulatedHue(unittest.TestCase):
|
|||||||
"""Stop the class."""
|
"""Stop the class."""
|
||||||
cls.hass.stop()
|
cls.hass.stop()
|
||||||
|
|
||||||
|
def test_upnp_discovery_basic(self):
|
||||||
|
"""Tests the UPnP basic discovery response."""
|
||||||
|
with patch("threading.Thread.__init__"):
|
||||||
|
upnp_responder_thread = emulated_hue.UPNPResponderThread(
|
||||||
|
"0.0.0.0", 80, True, "192.0.2.42", 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
"""Original request emitted by the Hue Bridge v1 app."""
|
||||||
|
request = """M-SEARCH * HTTP/1.1
|
||||||
|
HOST:239.255.255.250:1900
|
||||||
|
ST:ssdp:all
|
||||||
|
Man:"ssdp:discover"
|
||||||
|
MX:3
|
||||||
|
|
||||||
|
"""
|
||||||
|
encoded_request = request.replace("\n", "\r\n").encode("utf-8")
|
||||||
|
|
||||||
|
response = upnp_responder_thread._handle_request(encoded_request)
|
||||||
|
expected_response = """HTTP/1.1 200 OK
|
||||||
|
CACHE-CONTROL: max-age=60
|
||||||
|
EXT:
|
||||||
|
LOCATION: http://192.0.2.42:8080/description.xml
|
||||||
|
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
|
||||||
|
hue-bridgeid: 001788FFFE23BFC2
|
||||||
|
ST: urn:schemas-upnp-org:device:basic:1
|
||||||
|
USN: uuid:2f402f80-da50-11e1-9b23-001788255acc
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert expected_response.replace("\n", "\r\n").encode("utf-8") == response
|
||||||
|
|
||||||
|
def test_upnp_discovery_rootdevice(self):
|
||||||
|
"""Tests the UPnP rootdevice discovery response."""
|
||||||
|
with patch("threading.Thread.__init__"):
|
||||||
|
upnp_responder_thread = emulated_hue.UPNPResponderThread(
|
||||||
|
"0.0.0.0", 80, True, "192.0.2.42", 8080
|
||||||
|
)
|
||||||
|
|
||||||
|
"""Original request emitted by Busch-Jaeger free@home SysAP."""
|
||||||
|
request = """M-SEARCH * HTTP/1.1
|
||||||
|
HOST: 239.255.255.250:1900
|
||||||
|
MAN: "ssdp:discover"
|
||||||
|
MX: 40
|
||||||
|
ST: upnp:rootdevice
|
||||||
|
|
||||||
|
"""
|
||||||
|
encoded_request = request.replace("\n", "\r\n").encode("utf-8")
|
||||||
|
|
||||||
|
response = upnp_responder_thread._handle_request(encoded_request)
|
||||||
|
expected_response = """HTTP/1.1 200 OK
|
||||||
|
CACHE-CONTROL: max-age=60
|
||||||
|
EXT:
|
||||||
|
LOCATION: http://192.0.2.42:8080/description.xml
|
||||||
|
SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0
|
||||||
|
hue-bridgeid: 001788FFFE23BFC2
|
||||||
|
ST: upnp:rootdevice
|
||||||
|
USN: uuid:2f402f80-da50-11e1-9b23-001788255acc::upnp:rootdevice
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert expected_response.replace("\n", "\r\n").encode("utf-8") == response
|
||||||
|
|
||||||
def test_description_xml(self):
|
def test_description_xml(self):
|
||||||
"""Test the description."""
|
"""Test the description."""
|
||||||
result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5)
|
result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5)
|
||||||
|
@ -19,6 +19,7 @@ from homeassistant.components.homekit.accessories import HomeBridge
|
|||||||
from homeassistant.components.homekit.const import (
|
from homeassistant.components.homekit.const import (
|
||||||
AID_STORAGE,
|
AID_STORAGE,
|
||||||
BRIDGE_NAME,
|
BRIDGE_NAME,
|
||||||
|
BRIDGE_SERIAL_NUMBER,
|
||||||
CONF_AUTO_START,
|
CONF_AUTO_START,
|
||||||
CONF_ENTRY_INDEX,
|
CONF_ENTRY_INDEX,
|
||||||
CONF_SAFE_MODE,
|
CONF_SAFE_MODE,
|
||||||
@ -458,7 +459,7 @@ async def test_homekit_entity_filter(hass):
|
|||||||
assert mock_get_acc.called is False
|
assert mock_get_acc.called is False
|
||||||
|
|
||||||
|
|
||||||
async def test_homekit_start(hass, hk_driver, debounce_patcher):
|
async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher):
|
||||||
"""Test HomeKit start method."""
|
"""Test HomeKit start method."""
|
||||||
entry = await async_init_integration(hass)
|
entry = await async_init_integration(hass)
|
||||||
|
|
||||||
@ -480,6 +481,15 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher):
|
|||||||
homekit.driver = hk_driver
|
homekit.driver = hk_driver
|
||||||
homekit._filter = Mock(return_value=True)
|
homekit._filter = Mock(return_value=True)
|
||||||
|
|
||||||
|
connection = (device_registry.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")
|
||||||
|
bridge_with_wrong_mac = device_reg.async_get_or_create(
|
||||||
|
config_entry_id=entry.entry_id,
|
||||||
|
connections={connection},
|
||||||
|
manufacturer="Any",
|
||||||
|
name="Any",
|
||||||
|
model="Home Assistant HomeKit Bridge",
|
||||||
|
)
|
||||||
|
|
||||||
hass.states.async_set("light.demo", "on")
|
hass.states.async_set("light.demo", "on")
|
||||||
state = hass.states.async_all()[0]
|
state = hass.states.async_all()[0]
|
||||||
|
|
||||||
@ -505,6 +515,35 @@ async def test_homekit_start(hass, hk_driver, debounce_patcher):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert not hk_driver_start.called
|
assert not hk_driver_start.called
|
||||||
|
|
||||||
|
assert device_reg.async_get(bridge_with_wrong_mac.id) is None
|
||||||
|
|
||||||
|
device = device_reg.async_get_device(
|
||||||
|
{(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {}
|
||||||
|
)
|
||||||
|
assert device
|
||||||
|
formatted_mac = device_registry.format_mac(homekit.driver.state.mac)
|
||||||
|
assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections
|
||||||
|
|
||||||
|
# Start again to make sure the registry entry is kept
|
||||||
|
homekit.status = STATUS_READY
|
||||||
|
with patch(f"{PATH_HOMEKIT}.HomeKit.add_bridge_accessory") as mock_add_acc, patch(
|
||||||
|
f"{PATH_HOMEKIT}.show_setup_message"
|
||||||
|
) as mock_setup_msg, patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.add_accessory"
|
||||||
|
) as hk_driver_add_acc, patch(
|
||||||
|
"pyhap.accessory_driver.AccessoryDriver.start"
|
||||||
|
) as hk_driver_start:
|
||||||
|
await homekit.async_start()
|
||||||
|
|
||||||
|
device = device_reg.async_get_device(
|
||||||
|
{(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {}
|
||||||
|
)
|
||||||
|
assert device
|
||||||
|
formatted_mac = device_registry.format_mac(homekit.driver.state.mac)
|
||||||
|
assert (device_registry.CONNECTION_NETWORK_MAC, formatted_mac) in device.connections
|
||||||
|
|
||||||
|
assert len(device_reg.devices) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher):
|
async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher):
|
||||||
"""Test HomeKit start method."""
|
"""Test HomeKit start method."""
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
"""Test ONVIF config flow."""
|
"""Test ONVIF config flow."""
|
||||||
from asyncio import Future
|
|
||||||
|
|
||||||
from asynctest import MagicMock, patch
|
|
||||||
from onvif.exceptions import ONVIFError
|
from onvif.exceptions import ONVIFError
|
||||||
from zeep.exceptions import Fault
|
from zeep.exceptions import Fault
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow
|
from homeassistant import config_entries, data_entry_flow
|
||||||
from homeassistant.components.onvif import config_flow
|
from homeassistant.components.onvif import config_flow
|
||||||
|
|
||||||
|
from tests.async_mock import AsyncMock, MagicMock, patch
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
URN = "urn:uuid:123456789"
|
URN = "urn:uuid:123456789"
|
||||||
@ -17,6 +15,7 @@ PORT = 80
|
|||||||
USERNAME = "admin"
|
USERNAME = "admin"
|
||||||
PASSWORD = "12345"
|
PASSWORD = "12345"
|
||||||
MAC = "aa:bb:cc:dd:ee"
|
MAC = "aa:bb:cc:dd:ee"
|
||||||
|
SERIAL_NUMBER = "ABCDEFGHIJK"
|
||||||
|
|
||||||
DISCOVERY = [
|
DISCOVERY = [
|
||||||
{
|
{
|
||||||
@ -37,18 +36,25 @@ DISCOVERY = [
|
|||||||
|
|
||||||
|
|
||||||
def setup_mock_onvif_camera(
|
def setup_mock_onvif_camera(
|
||||||
mock_onvif_camera, with_h264=True, two_profiles=False, with_interfaces=True
|
mock_onvif_camera,
|
||||||
|
with_h264=True,
|
||||||
|
two_profiles=False,
|
||||||
|
with_interfaces=True,
|
||||||
|
with_serial=True,
|
||||||
):
|
):
|
||||||
"""Prepare mock onvif.ONVIFCamera."""
|
"""Prepare mock onvif.ONVIFCamera."""
|
||||||
devicemgmt = MagicMock()
|
devicemgmt = MagicMock()
|
||||||
|
|
||||||
|
device_info = MagicMock()
|
||||||
|
device_info.SerialNumber = SERIAL_NUMBER if with_serial else None
|
||||||
|
devicemgmt.GetDeviceInformation = AsyncMock(return_value=device_info)
|
||||||
|
|
||||||
interface = MagicMock()
|
interface = MagicMock()
|
||||||
interface.Enabled = True
|
interface.Enabled = True
|
||||||
interface.Info.HwAddress = MAC
|
interface.Info.HwAddress = MAC
|
||||||
|
|
||||||
devicemgmt.GetNetworkInterfaces.return_value = Future()
|
devicemgmt.GetNetworkInterfaces = AsyncMock(
|
||||||
devicemgmt.GetNetworkInterfaces.return_value.set_result(
|
return_value=[interface] if with_interfaces else []
|
||||||
[interface] if with_interfaces else []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
media_service = MagicMock()
|
media_service = MagicMock()
|
||||||
@ -58,11 +64,9 @@ def setup_mock_onvif_camera(
|
|||||||
profile2 = MagicMock()
|
profile2 = MagicMock()
|
||||||
profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG"
|
profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG"
|
||||||
|
|
||||||
media_service.GetProfiles.return_value = Future()
|
media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2])
|
||||||
media_service.GetProfiles.return_value.set_result([profile1, profile2])
|
|
||||||
|
|
||||||
mock_onvif_camera.update_xaddrs.return_value = Future()
|
mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True)
|
||||||
mock_onvif_camera.update_xaddrs.return_value.set_result(True)
|
|
||||||
mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt)
|
mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt)
|
||||||
mock_onvif_camera.create_media_service = MagicMock(return_value=media_service)
|
mock_onvif_camera.create_media_service = MagicMock(return_value=media_service)
|
||||||
|
|
||||||
@ -116,8 +120,7 @@ def setup_mock_discovery(
|
|||||||
|
|
||||||
def setup_mock_device(mock_device):
|
def setup_mock_device(mock_device):
|
||||||
"""Prepare mock ONVIFDevice."""
|
"""Prepare mock ONVIFDevice."""
|
||||||
mock_device.async_setup.return_value = Future()
|
mock_device.async_setup = AsyncMock(return_value=True)
|
||||||
mock_device.async_setup.return_value.set_result(True)
|
|
||||||
|
|
||||||
def mock_constructor(hass, config):
|
def mock_constructor(hass, config):
|
||||||
"""Fake the controller constructor."""
|
"""Fake the controller constructor."""
|
||||||
@ -390,11 +393,48 @@ async def test_flow_manual_entry(hass):
|
|||||||
|
|
||||||
|
|
||||||
async def test_flow_import_no_mac(hass):
|
async def test_flow_import_no_mac(hass):
|
||||||
"""Test that config flow fails when no MAC available."""
|
"""Test that config flow uses Serial Number when no MAC available."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.onvif.config_flow.get_device"
|
||||||
|
) as mock_onvif_camera, patch(
|
||||||
|
"homeassistant.components.onvif.ONVIFDevice"
|
||||||
|
) as mock_device:
|
||||||
|
setup_mock_onvif_camera(mock_onvif_camera, with_interfaces=False)
|
||||||
|
setup_mock_device(mock_device)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
config_flow.DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
config_flow.CONF_NAME: NAME,
|
||||||
|
config_flow.CONF_HOST: HOST,
|
||||||
|
config_flow.CONF_PORT: PORT,
|
||||||
|
config_flow.CONF_USERNAME: USERNAME,
|
||||||
|
config_flow.CONF_PASSWORD: PASSWORD,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == f"{NAME} - {SERIAL_NUMBER}"
|
||||||
|
assert result["data"] == {
|
||||||
|
config_flow.CONF_NAME: NAME,
|
||||||
|
config_flow.CONF_HOST: HOST,
|
||||||
|
config_flow.CONF_PORT: PORT,
|
||||||
|
config_flow.CONF_USERNAME: USERNAME,
|
||||||
|
config_flow.CONF_PASSWORD: PASSWORD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_import_no_mac_or_serial(hass):
|
||||||
|
"""Test that config flow fails when no MAC or Serial Number available."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.onvif.config_flow.get_device"
|
"homeassistant.components.onvif.config_flow.get_device"
|
||||||
) as mock_onvif_camera:
|
) as mock_onvif_camera:
|
||||||
setup_mock_onvif_camera(mock_onvif_camera, with_interfaces=False)
|
setup_mock_onvif_camera(
|
||||||
|
mock_onvif_camera, with_interfaces=False, with_serial=False
|
||||||
|
)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
config_flow.DOMAIN,
|
config_flow.DOMAIN,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user