mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 23:27:37 +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"]
|
||||
|
||||
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}
|
||||
|
||||
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
|
||||
under the monthly API limit.
|
||||
"""
|
||||
num_consumers = len(
|
||||
{
|
||||
config_entry
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.data.get(CONF_API_KEY) == api_key
|
||||
}
|
||||
)
|
||||
num_consumers = len(async_get_cloud_coordinators_by_api_key(hass, api_key))
|
||||
|
||||
# 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.:
|
||||
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)
|
||||
|
||||
|
||||
@callback
|
||||
def async_reset_coordinator_update_intervals(hass, update_interval):
|
||||
"""Update any existing data coordinators with a new update interval."""
|
||||
if not hass.data[DOMAIN][DATA_COORDINATOR]:
|
||||
return
|
||||
def async_get_cloud_coordinators_by_api_key(hass, api_key):
|
||||
"""Get all DataUpdateCoordinator objects related to a particular API key."""
|
||||
coordinators = []
|
||||
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
|
||||
|
||||
|
||||
@ -194,10 +210,6 @@ async def async_setup_entry(hass, config_entry):
|
||||
|
||||
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():
|
||||
"""Get new data from the API."""
|
||||
if CONF_CITY in config_entry.data:
|
||||
@ -219,14 +231,19 @@ async def async_setup_entry(hass, config_entry):
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
LOGGER,
|
||||
name="geography data",
|
||||
update_interval=update_interval,
|
||||
name=async_get_geography_id(config_entry.data),
|
||||
# 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,
|
||||
)
|
||||
|
||||
# Ensure any other, existing config entries that use this API key are updated
|
||||
# with the new scan interval:
|
||||
async_reset_coordinator_update_intervals(hass, update_interval)
|
||||
hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator
|
||||
async_sync_geo_coordinator_update_intervals(
|
||||
hass, config_entry.data[CONF_API_KEY]
|
||||
)
|
||||
|
||||
# Only geography-based entries have options:
|
||||
config_entry.add_update_listener(async_update_options)
|
||||
@ -251,13 +268,13 @@ async def async_setup_entry(hass, config_entry):
|
||||
hass,
|
||||
LOGGER,
|
||||
name="Node/Pro data",
|
||||
update_interval=DEFAULT_NODE_PRO_SCAN_INTERVAL,
|
||||
update_interval=DEFAULT_NODE_PRO_UPDATE_INTERVAL,
|
||||
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:
|
||||
hass.async_create_task(
|
||||
@ -317,6 +334,12 @@ async def async_unload_entry(hass, config_entry):
|
||||
)
|
||||
if unload_ok:
|
||||
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
|
||||
|
||||
|
@ -8,7 +8,7 @@ from .const import (
|
||||
CONF_INTEGRATION_TYPE,
|
||||
DATA_COORDINATOR,
|
||||
DOMAIN,
|
||||
INTEGRATION_TYPE_GEOGRAPHY,
|
||||
INTEGRATION_TYPE_NODE_PRO,
|
||||
)
|
||||
|
||||
ATTR_HUMIDITY = "humidity"
|
||||
@ -18,12 +18,12 @@ ATTR_VOC = "voc"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""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:
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] == INTEGRATION_TYPE_GEOGRAPHY:
|
||||
if config_entry.data[CONF_INTEGRATION_TYPE] != INTEGRATION_TYPE_NODE_PRO:
|
||||
return
|
||||
|
||||
coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id]
|
||||
|
||||
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
|
||||
# without being connected to the internet.
|
||||
if redirect_uri == "homeassistant://auth-callback" and client_id in (
|
||||
"https://www.home-assistant.io/android",
|
||||
"https://www.home-assistant.io/iOS",
|
||||
"https://home-assistant.io/android",
|
||||
"https://home-assistant.io/iOS",
|
||||
):
|
||||
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.components.http import HomeAssistantView
|
||||
|
||||
from .const import HUE_SERIAL_NUMBER, HUE_UUID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -42,8 +44,8 @@ class DescriptionXmlView(HomeAssistantView):
|
||||
<modelName>Philips hue bridge 2015</modelName>
|
||||
<modelNumber>BSB002</modelNumber>
|
||||
<modelURL>http://www.meethue.com</modelURL>
|
||||
<serialNumber>001788FFFE23BFC2</serialNumber>
|
||||
<UDN>uuid:2f402f80-da50-11e1-9b23-001788255acc</UDN>
|
||||
<serialNumber>{HUE_SERIAL_NUMBER}</serialNumber>
|
||||
<UDN>uuid:{HUE_UUID}</UDN>
|
||||
</device>
|
||||
</root>
|
||||
"""
|
||||
@ -70,21 +72,8 @@ class UPNPResponderThread(threading.Thread):
|
||||
self.host_ip_addr = host_ip_addr
|
||||
self.listen_port = listen_port
|
||||
self.upnp_bind_multicast = upnp_bind_multicast
|
||||
|
||||
# Note that the double newline at the end of
|
||||
# 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")
|
||||
self.advertise_ip = advertise_ip
|
||||
self.advertise_port = advertise_port
|
||||
|
||||
def run(self):
|
||||
"""Run the server."""
|
||||
@ -136,10 +125,13 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
|
||||
continue
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
def stop(self):
|
||||
@ -148,6 +140,31 @@ USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice
|
||||
self._interrupted = True
|
||||
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):
|
||||
"""Close a socket connection and logs its closure."""
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20200519.4"],
|
||||
"requirements": ["home-assistant-frontend==20200519.5"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
import ipaddress
|
||||
import logging
|
||||
import os
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
@ -49,6 +50,7 @@ from .const import (
|
||||
ATTR_SOFTWARE_VERSION,
|
||||
ATTR_VALUE,
|
||||
BRIDGE_NAME,
|
||||
BRIDGE_SERIAL_NUMBER,
|
||||
CONF_ADVERTISE_IP,
|
||||
CONF_AUTO_START,
|
||||
CONF_ENTITY_CONFIG,
|
||||
@ -434,6 +436,13 @@ class HomeKit:
|
||||
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)
|
||||
if self._safe_mode:
|
||||
_LOGGER.debug("Safe_mode selected for %s", self._name)
|
||||
@ -540,16 +549,45 @@ class HomeKit:
|
||||
@callback
|
||||
def _async_register_bridge(self, dev_reg):
|
||||
"""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(
|
||||
config_entry_id=self._entry_id,
|
||||
connections={
|
||||
(device_registry.CONNECTION_NETWORK_MAC, self.driver.state.mac)
|
||||
},
|
||||
identifiers={identifier},
|
||||
connections={connection},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=self._name,
|
||||
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):
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel
|
||||
type_cameras,
|
||||
|
@ -28,6 +28,7 @@ from .const import (
|
||||
DEVICE_INFO,
|
||||
DEVICE_MODEL,
|
||||
DOMAIN,
|
||||
LEGACY_DEVICE_MODEL,
|
||||
PV_API,
|
||||
PV_ROOM_DATA,
|
||||
PV_SHADE_DATA,
|
||||
@ -118,7 +119,7 @@ class PowerViewShade(ShadeEntity, CoverEntity):
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
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
|
||||
return supported_features
|
||||
|
||||
|
@ -4,5 +4,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
||||
"codeowners": ["@flz"],
|
||||
"requirements": ["iaqualink==0.3.1"]
|
||||
"requirements": ["iaqualink==0.3.3"]
|
||||
}
|
||||
|
@ -34,16 +34,25 @@ class HassAqualinkSensor(AqualinkEntity):
|
||||
return self.dev.label
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self) -> str:
|
||||
def unit_of_measurement(self) -> Optional[str]:
|
||||
"""Return the measurement unit for the sensor."""
|
||||
if self.dev.system.temp_unit == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
if self.dev.name.endswith("_temp"):
|
||||
if self.dev.system.temp_unit == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
return None
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
def state(self) -> Optional[str]:
|
||||
"""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
|
||||
def device_class(self) -> Optional[str]:
|
||||
|
@ -103,6 +103,8 @@ class EmailReader:
|
||||
|
||||
if message_data is None:
|
||||
return None
|
||||
if message_data[0] is None:
|
||||
return None
|
||||
raw_email = message_data[0][1]
|
||||
email_message = email.message_from_bytes(raw_email)
|
||||
return email_message
|
||||
@ -126,13 +128,22 @@ class EmailReader:
|
||||
self._last_id = int(message_uid)
|
||||
return self._fetch_message(message_uid)
|
||||
|
||||
return self._fetch_message(str(self._last_id))
|
||||
|
||||
except imaplib.IMAP4.error:
|
||||
_LOGGER.info("Connection to %s lost, attempting to reconnect", self._server)
|
||||
try:
|
||||
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:
|
||||
_LOGGER.error("Failed to reconnect")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class EmailContentSensor(Entity):
|
||||
"""Representation of an EMail sensor."""
|
||||
|
@ -2,6 +2,7 @@
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .device import ONVIFDevice
|
||||
from .models import Profile
|
||||
|
||||
@ -11,8 +12,8 @@ class ONVIFBaseEntity(Entity):
|
||||
|
||||
def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None:
|
||||
"""Initialize the ONVIF entity."""
|
||||
self.device = device
|
||||
self.profile = profile
|
||||
self.device: ONVIFDevice = device
|
||||
self.profile: Profile = profile
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
@ -22,10 +23,25 @@ class ONVIFBaseEntity(Entity):
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
"connections": {(CONNECTION_NETWORK_MAC, self.device.info.mac)},
|
||||
device_info = {
|
||||
"manufacturer": self.device.info.manufacturer,
|
||||
"model": self.device.info.model,
|
||||
"name": self.device.name,
|
||||
"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:
|
||||
"""Return a unique ID."""
|
||||
if self.profile.index:
|
||||
return f"{self.device.info.mac}_{self.profile.index}"
|
||||
return self.device.info.mac
|
||||
return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}"
|
||||
return self.device.info.mac or self.device.info.serial_number
|
||||
|
||||
@property
|
||||
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]
|
||||
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(
|
||||
step_id="auth",
|
||||
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()
|
||||
|
||||
try:
|
||||
device_mgmt = device.create_devicemgmt_service()
|
||||
|
||||
# Get the MAC address to use as the unique ID for the config flow
|
||||
if not self.device_id:
|
||||
devicemgmt = device.create_devicemgmt_service()
|
||||
network_interfaces = await devicemgmt.GetNetworkInterfaces()
|
||||
network_interfaces = await device_mgmt.GetNetworkInterfaces()
|
||||
for interface in network_interfaces:
|
||||
if interface.Enabled:
|
||||
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")
|
||||
|
||||
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:
|
||||
"""Warns if device and system date not synced."""
|
||||
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")
|
||||
try:
|
||||
system_date = dt_util.utcnow()
|
||||
device_time = await devicemgmt.GetSystemDateAndTime()
|
||||
device_time = await device_mgmt.GetSystemDateAndTime()
|
||||
if not device_time:
|
||||
LOGGER.debug(
|
||||
"""Couldn't get device '%s' date/time.
|
||||
@ -212,13 +212,22 @@ class ONVIFDevice:
|
||||
|
||||
async def async_get_device_info(self) -> DeviceInfo:
|
||||
"""Obtain information about this device."""
|
||||
devicemgmt = self.device.create_devicemgmt_service()
|
||||
device_info = await devicemgmt.GetDeviceInformation()
|
||||
device_mgmt = self.device.create_devicemgmt_service()
|
||||
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(
|
||||
device_info.Manufacturer,
|
||||
device_info.Model,
|
||||
device_info.FirmwareVersion,
|
||||
self.config_entry.unique_id,
|
||||
device_info.SerialNumber,
|
||||
mac,
|
||||
)
|
||||
|
||||
async def async_get_capabilities(self):
|
||||
@ -228,7 +237,7 @@ class ONVIFDevice:
|
||||
media_service = self.device.create_media_service()
|
||||
media_capabilities = await media_service.GetServiceCapabilities()
|
||||
snapshot = media_capabilities and media_capabilities.SnapshotUri
|
||||
except (ONVIFError, Fault):
|
||||
except (ONVIFError, Fault, ServerDisconnectedError):
|
||||
pass
|
||||
|
||||
pullpoint = False
|
||||
@ -415,7 +424,7 @@ class ONVIFDevice:
|
||||
"PTZ preset '%s' does not exist on device '%s'. Available Presets: %s",
|
||||
preset_val,
|
||||
self.name,
|
||||
profile.ptz.presets.join(", "),
|
||||
", ".join(profile.ptz.presets),
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -62,7 +62,8 @@ class EventManager:
|
||||
@callback
|
||||
def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None:
|
||||
"""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:
|
||||
self._unsub_refresh()
|
||||
@ -93,6 +94,8 @@ class EventManager:
|
||||
|
||||
async def async_stop(self) -> None:
|
||||
"""Unsubscribe from events."""
|
||||
self._listeners = []
|
||||
|
||||
if not self._subscription:
|
||||
return
|
||||
|
||||
@ -144,6 +147,10 @@ class EventManager:
|
||||
async def async_parse_messages(self, messages) -> None:
|
||||
"""Parse notification message."""
|
||||
for msg in messages:
|
||||
# Guard against empty message
|
||||
if not msg.Topic:
|
||||
continue
|
||||
|
||||
topic = msg.Topic._value_1
|
||||
parser = PARSERS.get(topic)
|
||||
if not parser:
|
||||
|
@ -10,6 +10,7 @@ class DeviceInfo:
|
||||
manufacturer: str = None
|
||||
model: str = None
|
||||
fw_version: str = None
|
||||
serial_number: 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."""
|
||||
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))
|
||||
|
||||
add_entities(covers, True)
|
||||
async_add_entities(covers, True)
|
||||
|
||||
|
||||
class OpenGarageCover(CoverEntity):
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "roku",
|
||||
"name": "Roku",
|
||||
"documentation": "https://www.home-assistant.io/integrations/roku",
|
||||
"requirements": ["rokuecp==0.4.1"],
|
||||
"requirements": ["rokuecp==0.4.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "roku:ecp",
|
||||
|
@ -171,15 +171,18 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""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:
|
||||
"""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:
|
||||
"""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:
|
||||
"""Send previous track command."""
|
||||
|
@ -2,6 +2,6 @@
|
||||
"domain": "velux",
|
||||
"name": "Velux",
|
||||
"documentation": "https://www.home-assistant.io/integrations/velux",
|
||||
"requirements": ["pyvlx==0.2.14"],
|
||||
"requirements": ["pyvlx==0.2.16"],
|
||||
"codeowners": ["@Julius2342"]
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""Constants used by Home Assistant components."""
|
||||
MAJOR_VERSION = 0
|
||||
MINOR_VERSION = 110
|
||||
PATCH_VERSION = "2"
|
||||
PATCH_VERSION = "3"
|
||||
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__ = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER = (3, 7, 0)
|
||||
|
@ -12,7 +12,7 @@ cryptography==2.9.2
|
||||
defusedxml==0.6.0
|
||||
distro==1.5.0
|
||||
hass-nabucasa==0.34.3
|
||||
home-assistant-frontend==20200519.4
|
||||
home-assistant-frontend==20200519.5
|
||||
importlib-metadata==1.6.0
|
||||
jinja2>=2.11.1
|
||||
netdisco==2.6.0
|
||||
|
@ -731,7 +731,7 @@ hole==0.5.1
|
||||
holidays==0.10.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200519.4
|
||||
home-assistant-frontend==20200519.5
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -764,7 +764,7 @@ hydrawiser==0.1.1
|
||||
iammeter==0.1.7
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.3.1
|
||||
iaqualink==0.3.3
|
||||
|
||||
# homeassistant.components.watson_tts
|
||||
ibm-watson==4.0.1
|
||||
@ -1802,7 +1802,7 @@ pyvesync==1.1.0
|
||||
pyvizio==0.1.47
|
||||
|
||||
# homeassistant.components.velux
|
||||
pyvlx==0.2.14
|
||||
pyvlx==0.2.16
|
||||
|
||||
# homeassistant.components.html5
|
||||
pywebpush==1.9.2
|
||||
@ -1871,7 +1871,7 @@ rjpl==0.3.5
|
||||
rocketchat-API==0.6.1
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.4.1
|
||||
rokuecp==0.4.2
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.1
|
||||
|
@ -312,7 +312,7 @@ hole==0.5.1
|
||||
holidays==0.10.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20200519.4
|
||||
home-assistant-frontend==20200519.5
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.10
|
||||
@ -331,7 +331,7 @@ httplib2==0.10.3
|
||||
huawei-lte-api==1.4.12
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.3.1
|
||||
iaqualink==0.3.3
|
||||
|
||||
# homeassistant.components.influxdb
|
||||
influxdb==5.2.3
|
||||
@ -762,7 +762,7 @@ rflink==0.0.52
|
||||
ring_doorbell==0.6.0
|
||||
|
||||
# homeassistant.components.roku
|
||||
rokuecp==0.4.1
|
||||
rokuecp==0.4.2
|
||||
|
||||
# homeassistant.components.roomba
|
||||
roombapy==1.6.1
|
||||
|
@ -166,8 +166,7 @@ async def test_find_link_tag_max_size(hass, mock_session):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client_id",
|
||||
["https://www.home-assistant.io/android", "https://www.home-assistant.io/iOS"],
|
||||
"client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"],
|
||||
)
|
||||
async def test_verify_redirect_uri_android_ios(client_id):
|
||||
"""Test that we verify redirect uri correctly for Android/iOS."""
|
||||
|
@ -48,6 +48,66 @@ class TestEmulatedHue(unittest.TestCase):
|
||||
"""Stop the class."""
|
||||
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):
|
||||
"""Test the description."""
|
||||
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 (
|
||||
AID_STORAGE,
|
||||
BRIDGE_NAME,
|
||||
BRIDGE_SERIAL_NUMBER,
|
||||
CONF_AUTO_START,
|
||||
CONF_ENTRY_INDEX,
|
||||
CONF_SAFE_MODE,
|
||||
@ -458,7 +459,7 @@ async def test_homekit_entity_filter(hass):
|
||||
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."""
|
||||
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._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")
|
||||
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()
|
||||
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):
|
||||
"""Test HomeKit start method."""
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""Test ONVIF config flow."""
|
||||
from asyncio import Future
|
||||
|
||||
from asynctest import MagicMock, patch
|
||||
from onvif.exceptions import ONVIFError
|
||||
from zeep.exceptions import Fault
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.onvif import config_flow
|
||||
|
||||
from tests.async_mock import AsyncMock, MagicMock, patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
URN = "urn:uuid:123456789"
|
||||
@ -17,6 +15,7 @@ PORT = 80
|
||||
USERNAME = "admin"
|
||||
PASSWORD = "12345"
|
||||
MAC = "aa:bb:cc:dd:ee"
|
||||
SERIAL_NUMBER = "ABCDEFGHIJK"
|
||||
|
||||
DISCOVERY = [
|
||||
{
|
||||
@ -37,18 +36,25 @@ DISCOVERY = [
|
||||
|
||||
|
||||
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."""
|
||||
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.Enabled = True
|
||||
interface.Info.HwAddress = MAC
|
||||
|
||||
devicemgmt.GetNetworkInterfaces.return_value = Future()
|
||||
devicemgmt.GetNetworkInterfaces.return_value.set_result(
|
||||
[interface] if with_interfaces else []
|
||||
devicemgmt.GetNetworkInterfaces = AsyncMock(
|
||||
return_value=[interface] if with_interfaces else []
|
||||
)
|
||||
|
||||
media_service = MagicMock()
|
||||
@ -58,11 +64,9 @@ def setup_mock_onvif_camera(
|
||||
profile2 = MagicMock()
|
||||
profile2.VideoEncoderConfiguration.Encoding = "H264" if two_profiles else "MJPEG"
|
||||
|
||||
media_service.GetProfiles.return_value = Future()
|
||||
media_service.GetProfiles.return_value.set_result([profile1, profile2])
|
||||
media_service.GetProfiles = AsyncMock(return_value=[profile1, profile2])
|
||||
|
||||
mock_onvif_camera.update_xaddrs.return_value = Future()
|
||||
mock_onvif_camera.update_xaddrs.return_value.set_result(True)
|
||||
mock_onvif_camera.update_xaddrs = AsyncMock(return_value=True)
|
||||
mock_onvif_camera.create_devicemgmt_service = MagicMock(return_value=devicemgmt)
|
||||
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):
|
||||
"""Prepare mock ONVIFDevice."""
|
||||
mock_device.async_setup.return_value = Future()
|
||||
mock_device.async_setup.return_value.set_result(True)
|
||||
mock_device.async_setup = AsyncMock(return_value=True)
|
||||
|
||||
def mock_constructor(hass, config):
|
||||
"""Fake the controller constructor."""
|
||||
@ -390,11 +393,48 @@ async def test_flow_manual_entry(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(
|
||||
"homeassistant.components.onvif.config_flow.get_device"
|
||||
) 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(
|
||||
config_flow.DOMAIN,
|
||||
|
Loading…
x
Reference in New Issue
Block a user