mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Config flow for doorbird (#33165)
* Config flow for doorbird * Discoverable via zeroconf * Fix zeroconf test * add missing return * Add a test for legacy over ride url (will go away when refactored to cloud hooks) * Update homeassistant/components/doorbird/__init__.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * without getting the hooks its not so useful * Update homeassistant/components/doorbird/config_flow.py Co-Authored-By: Paulus Schoutsen <paulus@home-assistant.io> * fix copy pasta * remove identifiers since its in connections * self review fixes Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
49ebea2be3
commit
f9a7c64106
@ -86,7 +86,7 @@ homeassistant/components/device_automation/* @home-assistant/core
|
|||||||
homeassistant/components/digital_ocean/* @fabaff
|
homeassistant/components/digital_ocean/* @fabaff
|
||||||
homeassistant/components/directv/* @ctalkington
|
homeassistant/components/directv/* @ctalkington
|
||||||
homeassistant/components/discogs/* @thibmaek
|
homeassistant/components/discogs/* @thibmaek
|
||||||
homeassistant/components/doorbird/* @oblogic7
|
homeassistant/components/doorbird/* @oblogic7 @bdraco
|
||||||
homeassistant/components/dsmr_reader/* @depl0y
|
homeassistant/components/dsmr_reader/* @depl0y
|
||||||
homeassistant/components/dweet/* @fabaff
|
homeassistant/components/dweet/* @fabaff
|
||||||
homeassistant/components/dynalite/* @ziv1234
|
homeassistant/components/dynalite/* @ziv1234
|
||||||
|
34
homeassistant/components/doorbird/.translations/en.json
Normal file
34
homeassistant/components/doorbird/.translations/en.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"options" : {
|
||||||
|
"step" : {
|
||||||
|
"init" : {
|
||||||
|
"data" : {
|
||||||
|
"events" : "Comma separated list of events."
|
||||||
|
},
|
||||||
|
"description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config" : {
|
||||||
|
"step" : {
|
||||||
|
"user" : {
|
||||||
|
"title" : "Connect to the DoorBird",
|
||||||
|
"data" : {
|
||||||
|
"password" : "Password",
|
||||||
|
"host" : "Host (IP Address)",
|
||||||
|
"name" : "Device Name",
|
||||||
|
"username" : "Username"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort" : {
|
||||||
|
"already_configured" : "This DoorBird is already configured"
|
||||||
|
},
|
||||||
|
"title" : "DoorBird",
|
||||||
|
"error" : {
|
||||||
|
"invalid_auth" : "Invalid authentication",
|
||||||
|
"unknown" : "Unexpected error",
|
||||||
|
"cannot_connect" : "Failed to connect, please try again"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
"""Support for DoorBird devices."""
|
"""Support for DoorBird devices."""
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import urllib
|
||||||
from urllib.error import HTTPError
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
from doorbirdpy import DoorBird
|
from doorbirdpy import DoorBird
|
||||||
@ -7,6 +9,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.logbook import log_entry
|
from homeassistant.components.logbook import log_entry
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DEVICES,
|
CONF_DEVICES,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
@ -15,17 +18,19 @@ from homeassistant.const import (
|
|||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import dt as dt_util, slugify
|
from homeassistant.util import dt as dt_util, slugify
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
from .const import CONF_EVENTS, DOMAIN, DOOR_STATION, DOOR_STATION_INFO, PLATFORMS
|
||||||
|
from .util import get_doorstation_by_token
|
||||||
|
|
||||||
DOMAIN = "doorbird"
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
API_URL = f"/api/{DOMAIN}"
|
API_URL = f"/api/{DOMAIN}"
|
||||||
|
|
||||||
CONF_CUSTOM_URL = "hass_url_override"
|
CONF_CUSTOM_URL = "hass_url_override"
|
||||||
CONF_EVENTS = "events"
|
|
||||||
|
|
||||||
RESET_DEVICE_FAVORITES = "doorbird_reset_favorites"
|
RESET_DEVICE_FAVORITES = "doorbird_reset_favorites"
|
||||||
|
|
||||||
@ -51,72 +56,24 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
"""Set up the DoorBird component."""
|
"""Set up the DoorBird component."""
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
# Provide an endpoint for the doorstations to call to trigger events
|
# Provide an endpoint for the doorstations to call to trigger events
|
||||||
hass.http.register_view(DoorBirdRequestView)
|
hass.http.register_view(DoorBirdRequestView)
|
||||||
|
|
||||||
doorstations = []
|
if DOMAIN in config and CONF_DEVICES in config[DOMAIN]:
|
||||||
|
for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]):
|
||||||
|
if CONF_NAME not in doorstation_config:
|
||||||
|
doorstation_config[CONF_NAME] = f"DoorBird {index + 1}"
|
||||||
|
|
||||||
for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]):
|
hass.async_create_task(
|
||||||
device_ip = doorstation_config.get(CONF_HOST)
|
hass.config_entries.flow.async_init(
|
||||||
username = doorstation_config.get(CONF_USERNAME)
|
DOMAIN, context={"source": SOURCE_IMPORT}, data=doorstation_config,
|
||||||
password = doorstation_config.get(CONF_PASSWORD)
|
|
||||||
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
|
|
||||||
events = doorstation_config.get(CONF_EVENTS)
|
|
||||||
token = doorstation_config.get(CONF_TOKEN)
|
|
||||||
name = doorstation_config.get(CONF_NAME) or f"DoorBird {index + 1}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
device = DoorBird(device_ip, username, password)
|
|
||||||
status = device.ready()
|
|
||||||
except OSError as oserr:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to setup doorbird at %s: %s; not retrying", device_ip, oserr
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if status[0]:
|
|
||||||
doorstation = ConfiguredDoorBird(device, name, events, custom_url, token)
|
|
||||||
doorstations.append(doorstation)
|
|
||||||
_LOGGER.info(
|
|
||||||
'Connected to DoorBird "%s" as %s@%s',
|
|
||||||
doorstation.name,
|
|
||||||
username,
|
|
||||||
device_ip,
|
|
||||||
)
|
|
||||||
elif status[1] == 401:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Could not connect to DoorBird as %s@%s: Error %s",
|
|
||||||
username,
|
|
||||||
device_ip,
|
|
||||||
str(status[1]),
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Subscribe to doorbell or motion events
|
|
||||||
if events:
|
|
||||||
try:
|
|
||||||
doorstation.register_events(hass)
|
|
||||||
except HTTPError:
|
|
||||||
hass.components.persistent_notification.create(
|
|
||||||
"Doorbird configuration failed. Please verify that API "
|
|
||||||
"Operator permission is enabled for the Doorbird user. "
|
|
||||||
"A restart will be required once permissions have been "
|
|
||||||
"verified.",
|
|
||||||
title="Doorbird Configuration Failure",
|
|
||||||
notification_id="doorbird_schedule_error",
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
return False
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = doorstations
|
|
||||||
|
|
||||||
def _reset_device_favorites_handler(event):
|
def _reset_device_favorites_handler(event):
|
||||||
"""Handle clearing favorites on device."""
|
"""Handle clearing favorites on device."""
|
||||||
@ -129,6 +86,7 @@ def setup(hass, config):
|
|||||||
|
|
||||||
if doorstation is None:
|
if doorstation is None:
|
||||||
_LOGGER.error("Device not found for provided token.")
|
_LOGGER.error("Device not found for provided token.")
|
||||||
|
return
|
||||||
|
|
||||||
# Clear webhooks
|
# Clear webhooks
|
||||||
favorites = doorstation.device.favorites()
|
favorites = doorstation.device.favorites()
|
||||||
@ -137,16 +95,126 @@ def setup(hass, config):
|
|||||||
for favorite_id in favorites[favorite_type]:
|
for favorite_id in favorites[favorite_type]:
|
||||||
doorstation.device.delete_favorite(favorite_type, favorite_id)
|
doorstation.device.delete_favorite(favorite_type, favorite_id)
|
||||||
|
|
||||||
hass.bus.listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler)
|
hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_doorstation_by_token(hass, token):
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
"""Get doorstation by slug."""
|
"""Set up DoorBird from a config entry."""
|
||||||
for doorstation in hass.data[DOMAIN]:
|
|
||||||
if token == doorstation.token:
|
_async_import_options_from_data_if_missing(hass, entry)
|
||||||
return doorstation
|
|
||||||
|
doorstation_config = entry.data
|
||||||
|
doorstation_options = entry.options
|
||||||
|
config_entry_id = entry.entry_id
|
||||||
|
|
||||||
|
device_ip = doorstation_config[CONF_HOST]
|
||||||
|
username = doorstation_config[CONF_USERNAME]
|
||||||
|
password = doorstation_config[CONF_PASSWORD]
|
||||||
|
|
||||||
|
device = DoorBird(device_ip, username, password)
|
||||||
|
try:
|
||||||
|
status = await hass.async_add_executor_job(device.ready)
|
||||||
|
info = await hass.async_add_executor_job(device.info)
|
||||||
|
except urllib.error.HTTPError as err:
|
||||||
|
if err.code == 401:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
except OSError as oserr:
|
||||||
|
_LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr)
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
if not status[0]:
|
||||||
|
_LOGGER.error(
|
||||||
|
"Could not connect to DoorBird as %s@%s: Error %s",
|
||||||
|
username,
|
||||||
|
device_ip,
|
||||||
|
str(status[1]),
|
||||||
|
)
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
token = doorstation_config.get(CONF_TOKEN, config_entry_id)
|
||||||
|
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
|
||||||
|
name = doorstation_config.get(CONF_NAME)
|
||||||
|
events = doorstation_options.get(CONF_EVENTS, [])
|
||||||
|
doorstation = ConfiguredDoorBird(device, name, events, custom_url, token)
|
||||||
|
# Subscribe to doorbell or motion events
|
||||||
|
if not await _async_register_events(hass, doorstation):
|
||||||
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
hass.data[DOMAIN][config_entry_id] = {
|
||||||
|
DOOR_STATION: doorstation,
|
||||||
|
DOOR_STATION_INFO: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.add_update_listener(_update_listener)
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_register_events(hass, doorstation):
|
||||||
|
try:
|
||||||
|
await hass.async_add_executor_job(doorstation.register_events, hass)
|
||||||
|
except HTTPError:
|
||||||
|
hass.components.persistent_notification.create(
|
||||||
|
"Doorbird configuration failed. Please verify that API "
|
||||||
|
"Operator permission is enabled for the Doorbird user. "
|
||||||
|
"A restart will be required once permissions have been "
|
||||||
|
"verified.",
|
||||||
|
title="Doorbird Configuration Failure",
|
||||||
|
notification_id="doorbird_schedule_error",
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Handle options update."""
|
||||||
|
config_entry_id = entry.entry_id
|
||||||
|
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
|
||||||
|
|
||||||
|
doorstation.events = entry.options[CONF_EVENTS]
|
||||||
|
# Subscribe to doorbell or motion events
|
||||||
|
await _async_register_events(hass, doorstation)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
options = dict(entry.options)
|
||||||
|
modified = False
|
||||||
|
for importable_option in [CONF_EVENTS]:
|
||||||
|
if importable_option not in entry.options and importable_option in entry.data:
|
||||||
|
options[importable_option] = entry.data[importable_option]
|
||||||
|
modified = True
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
|
||||||
|
|
||||||
class ConfiguredDoorBird:
|
class ConfiguredDoorBird:
|
||||||
@ -157,7 +225,7 @@ class ConfiguredDoorBird:
|
|||||||
self._name = name
|
self._name = name
|
||||||
self._device = device
|
self._device = device
|
||||||
self._custom_url = custom_url
|
self._custom_url = custom_url
|
||||||
self._events = events
|
self.events = events
|
||||||
self._token = token
|
self._token = token
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -189,7 +257,7 @@ class ConfiguredDoorBird:
|
|||||||
if self.custom_url is not None:
|
if self.custom_url is not None:
|
||||||
hass_url = self.custom_url
|
hass_url = self.custom_url
|
||||||
|
|
||||||
for event in self._events:
|
for event in self.events:
|
||||||
event = self._get_event_name(event)
|
event = self._get_event_name(event)
|
||||||
|
|
||||||
self._register_event(hass_url, event)
|
self._register_event(hass_url, event)
|
||||||
|
@ -10,46 +10,69 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN as DOORBIRD_DOMAIN
|
from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO
|
||||||
|
from .entity import DoorBirdEntity
|
||||||
|
|
||||||
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
|
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2)
|
||||||
_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
|
_LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30)
|
||||||
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
|
_LIVE_INTERVAL = datetime.timedelta(seconds=45)
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
_TIMEOUT = 10 # seconds
|
_TIMEOUT = 15 # seconds
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the DoorBird camera platform."""
|
"""Set up the DoorBird camera platform."""
|
||||||
for doorstation in hass.data[DOORBIRD_DOMAIN]:
|
config_entry_id = config_entry.entry_id
|
||||||
device = doorstation.device
|
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
|
||||||
async_add_entities(
|
doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO]
|
||||||
[
|
device = doorstation.device
|
||||||
DoorBirdCamera(
|
|
||||||
device.live_image_url,
|
async_add_entities(
|
||||||
f"{doorstation.name} Live",
|
[
|
||||||
_LIVE_INTERVAL,
|
DoorBirdCamera(
|
||||||
device.rtsp_live_video_url,
|
doorstation,
|
||||||
),
|
doorstation_info,
|
||||||
DoorBirdCamera(
|
device.live_image_url,
|
||||||
device.history_image_url(1, "doorbell"),
|
"live",
|
||||||
f"{doorstation.name} Last Ring",
|
f"{doorstation.name} Live",
|
||||||
_LAST_VISITOR_INTERVAL,
|
_LIVE_INTERVAL,
|
||||||
),
|
device.rtsp_live_video_url,
|
||||||
DoorBirdCamera(
|
),
|
||||||
device.history_image_url(1, "motionsensor"),
|
DoorBirdCamera(
|
||||||
f"{doorstation.name} Last Motion",
|
doorstation,
|
||||||
_LAST_MOTION_INTERVAL,
|
doorstation_info,
|
||||||
),
|
device.history_image_url(1, "doorbell"),
|
||||||
]
|
"last_ring",
|
||||||
)
|
f"{doorstation.name} Last Ring",
|
||||||
|
_LAST_VISITOR_INTERVAL,
|
||||||
|
),
|
||||||
|
DoorBirdCamera(
|
||||||
|
doorstation,
|
||||||
|
doorstation_info,
|
||||||
|
device.history_image_url(1, "motionsensor"),
|
||||||
|
"last_motion",
|
||||||
|
f"{doorstation.name} Last Motion",
|
||||||
|
_LAST_MOTION_INTERVAL,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DoorBirdCamera(Camera):
|
class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||||
"""The camera on a DoorBird device."""
|
"""The camera on a DoorBird device."""
|
||||||
|
|
||||||
def __init__(self, url, name, interval=None, stream_url=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
doorstation,
|
||||||
|
doorstation_info,
|
||||||
|
url,
|
||||||
|
camera_id,
|
||||||
|
name,
|
||||||
|
interval=None,
|
||||||
|
stream_url=None,
|
||||||
|
):
|
||||||
"""Initialize the camera on a DoorBird device."""
|
"""Initialize the camera on a DoorBird device."""
|
||||||
|
super().__init__(doorstation, doorstation_info)
|
||||||
self._url = url
|
self._url = url
|
||||||
self._stream_url = stream_url
|
self._stream_url = stream_url
|
||||||
self._name = name
|
self._name = name
|
||||||
@ -57,12 +80,17 @@ class DoorBirdCamera(Camera):
|
|||||||
self._supported_features = SUPPORT_STREAM if self._stream_url else 0
|
self._supported_features = SUPPORT_STREAM if self._stream_url else 0
|
||||||
self._interval = interval or datetime.timedelta
|
self._interval = interval or datetime.timedelta
|
||||||
self._last_update = datetime.datetime.min
|
self._last_update = datetime.datetime.min
|
||||||
super().__init__()
|
self._unique_id = f"{self._mac_addr}_{camera_id}"
|
||||||
|
|
||||||
async def stream_source(self):
|
async def stream_source(self):
|
||||||
"""Return the stream source."""
|
"""Return the stream source."""
|
||||||
return self._stream_url
|
return self._stream_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Camera Unique id."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
"""Return supported features."""
|
"""Return supported features."""
|
||||||
@ -89,8 +117,10 @@ class DoorBirdCamera(Camera):
|
|||||||
self._last_update = now
|
self._last_update = now
|
||||||
return self._last_image
|
return self._last_image
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.error("Camera image timed out")
|
_LOGGER.error("DoorBird %s: Camera image timed out", self._name)
|
||||||
return self._last_image
|
return self._last_image
|
||||||
except aiohttp.ClientError as error:
|
except aiohttp.ClientError as error:
|
||||||
_LOGGER.error("Error getting camera image: %s", error)
|
_LOGGER.error(
|
||||||
|
"DoorBird %s: Error getting camera image: %s", self._name, error
|
||||||
|
)
|
||||||
return self._last_image
|
return self._last_image
|
||||||
|
156
homeassistant/components/doorbird/config_flow.py
Normal file
156
homeassistant/components/doorbird/config_flow.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""Config flow for DoorBird integration."""
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from doorbirdpy import DoorBird
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core, exceptions
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
|
||||||
|
from .const import CONF_EVENTS, DOORBIRD_OUI
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
from .util import get_mac_address_from_doorstation_info
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_with_defaults(host=None, name=None):
|
||||||
|
return vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST, default=host): str,
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_NAME, default=name): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||||
|
try:
|
||||||
|
status = await hass.async_add_executor_job(device.ready)
|
||||||
|
info = await hass.async_add_executor_job(device.info)
|
||||||
|
except urllib.error.HTTPError as err:
|
||||||
|
if err.code == 401:
|
||||||
|
raise InvalidAuth
|
||||||
|
raise CannotConnect
|
||||||
|
except OSError:
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
if not status[0]:
|
||||||
|
raise CannotConnect
|
||||||
|
|
||||||
|
mac_addr = get_mac_address_from_doorstation_info(info)
|
||||||
|
|
||||||
|
# Return info that you want to store in the config entry.
|
||||||
|
return {"title": data[CONF_HOST], "mac_addr": mac_addr}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for DoorBird."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the DoorBird config flow."""
|
||||||
|
self.discovery_schema = {}
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
if "base" not in errors:
|
||||||
|
await self.async_set_unique_id(info["mac_addr"])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
|
||||||
|
data = self.discovery_schema or _schema_with_defaults()
|
||||||
|
return self.async_show_form(step_id="user", data_schema=data, errors=errors)
|
||||||
|
|
||||||
|
async def async_step_zeroconf(self, discovery_info):
|
||||||
|
"""Prepare configuration for a discovered doorbird device."""
|
||||||
|
macaddress = discovery_info["properties"]["macaddress"]
|
||||||
|
|
||||||
|
if macaddress[:6] != DOORBIRD_OUI:
|
||||||
|
return self.async_abort(reason="not_doorbird_device")
|
||||||
|
|
||||||
|
await self.async_set_unique_id(macaddress)
|
||||||
|
|
||||||
|
self._abort_if_unique_id_configured(
|
||||||
|
updates={CONF_HOST: discovery_info[CONF_HOST]}
|
||||||
|
)
|
||||||
|
|
||||||
|
chop_ending = "._axis-video._tcp.local."
|
||||||
|
friendly_hostname = discovery_info["name"]
|
||||||
|
if friendly_hostname.endswith(chop_ending):
|
||||||
|
friendly_hostname = friendly_hostname[: -len(chop_ending)]
|
||||||
|
|
||||||
|
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
|
||||||
|
self.context["title_placeholders"] = {
|
||||||
|
CONF_NAME: friendly_hostname,
|
||||||
|
CONF_HOST: discovery_info[CONF_HOST],
|
||||||
|
}
|
||||||
|
self.discovery_schema = _schema_with_defaults(
|
||||||
|
host=discovery_info[CONF_HOST], name=friendly_hostname
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
async def async_step_import(self, user_input):
|
||||||
|
"""Handle import."""
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Handle a option flow for doorbird."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input=None):
|
||||||
|
"""Handle options flow."""
|
||||||
|
if user_input is not None:
|
||||||
|
events = [event.strip() for event in user_input[CONF_EVENTS].split(",")]
|
||||||
|
|
||||||
|
return self.async_create_entry(title="", data={CONF_EVENTS: events})
|
||||||
|
|
||||||
|
current_events = self.config_entry.options.get(CONF_EVENTS, [])
|
||||||
|
|
||||||
|
# We convert to a comma separated list for the UI
|
||||||
|
# since there really isn't anything better
|
||||||
|
options_schema = vol.Schema(
|
||||||
|
{vol.Optional(CONF_EVENTS, default=", ".join(current_events)): str}
|
||||||
|
)
|
||||||
|
return self.async_show_form(step_id="init", data_schema=options_schema)
|
||||||
|
|
||||||
|
|
||||||
|
class CannotConnect(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
|
"""Error to indicate there is invalid auth."""
|
17
homeassistant/components/doorbird/const.py
Normal file
17
homeassistant/components/doorbird/const.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""The DoorBird integration constants."""
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN = "doorbird"
|
||||||
|
PLATFORMS = ["switch", "camera"]
|
||||||
|
DOOR_STATION = "door_station"
|
||||||
|
DOOR_STATION_INFO = "door_station_info"
|
||||||
|
CONF_EVENTS = "events"
|
||||||
|
MANUFACTURER = "Bird Home Automation Group"
|
||||||
|
DOORBIRD_OUI = "1CCAE3"
|
||||||
|
|
||||||
|
DOORBIRD_INFO_KEY_FIRMWARE = "FIRMWARE"
|
||||||
|
DOORBIRD_INFO_KEY_BUILD_NUMBER = "BUILD_NUMBER"
|
||||||
|
DOORBIRD_INFO_KEY_DEVICE_TYPE = "DEVICE-TYPE"
|
||||||
|
DOORBIRD_INFO_KEY_RELAYS = "RELAYS"
|
||||||
|
DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR"
|
||||||
|
DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR"
|
36
homeassistant/components/doorbird/entity.py
Normal file
36
homeassistant/components/doorbird/entity.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""The DoorBird integration base entity."""
|
||||||
|
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DOORBIRD_INFO_KEY_BUILD_NUMBER,
|
||||||
|
DOORBIRD_INFO_KEY_DEVICE_TYPE,
|
||||||
|
DOORBIRD_INFO_KEY_FIRMWARE,
|
||||||
|
MANUFACTURER,
|
||||||
|
)
|
||||||
|
from .util import get_mac_address_from_doorstation_info
|
||||||
|
|
||||||
|
|
||||||
|
class DoorBirdEntity(Entity):
|
||||||
|
"""Base class for doorbird entities."""
|
||||||
|
|
||||||
|
def __init__(self, doorstation, doorstation_info):
|
||||||
|
"""Initialize the entity."""
|
||||||
|
super().__init__()
|
||||||
|
self._doorstation_info = doorstation_info
|
||||||
|
self._doorstation = doorstation
|
||||||
|
self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Doorbird device info."""
|
||||||
|
firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE]
|
||||||
|
firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER]
|
||||||
|
return {
|
||||||
|
"connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_addr)},
|
||||||
|
"name": self._doorstation.name,
|
||||||
|
"manufacturer": MANUFACTURER,
|
||||||
|
"sw_version": f"{firmware} {firmware_build}",
|
||||||
|
"model": self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE],
|
||||||
|
}
|
@ -2,7 +2,16 @@
|
|||||||
"domain": "doorbird",
|
"domain": "doorbird",
|
||||||
"name": "DoorBird",
|
"name": "DoorBird",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||||
"requirements": ["doorbirdpy==2.0.8"],
|
"requirements": [
|
||||||
"dependencies": ["http", "logbook"],
|
"doorbirdpy==2.0.8"
|
||||||
"codeowners": ["@oblogic7"]
|
],
|
||||||
|
"dependencies": [
|
||||||
|
"http",
|
||||||
|
"logbook"
|
||||||
|
],
|
||||||
|
"zeroconf": ["_axis-video._tcp.local."],
|
||||||
|
"codeowners": [
|
||||||
|
"@oblogic7", "@bdraco"
|
||||||
|
],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
34
homeassistant/components/doorbird/strings.json
Normal file
34
homeassistant/components/doorbird/strings.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"options" : {
|
||||||
|
"step" : {
|
||||||
|
"init" : {
|
||||||
|
"data" : {
|
||||||
|
"events" : "Comma separated list of events."
|
||||||
|
},
|
||||||
|
"description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config" : {
|
||||||
|
"step" : {
|
||||||
|
"user" : {
|
||||||
|
"title" : "Connect to the DoorBird",
|
||||||
|
"data" : {
|
||||||
|
"password" : "Password",
|
||||||
|
"host" : "Host (IP Address)",
|
||||||
|
"name" : "Device Name",
|
||||||
|
"username" : "Username"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort" : {
|
||||||
|
"already_configured" : "This DoorBird is already configured"
|
||||||
|
},
|
||||||
|
"title" : "DoorBird",
|
||||||
|
"error" : {
|
||||||
|
"invalid_auth" : "Invalid authentication",
|
||||||
|
"unknown" : "Unexpected error",
|
||||||
|
"cannot_connect" : "Failed to connect, please try again"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,33 +5,38 @@ import logging
|
|||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.switch import SwitchDevice
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN as DOORBIRD_DOMAIN
|
from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO
|
||||||
|
from .entity import DoorBirdEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
IR_RELAY = "__ir_light__"
|
IR_RELAY = "__ir_light__"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the DoorBird switch platform."""
|
"""Set up the DoorBird switch platform."""
|
||||||
switches = []
|
entities = []
|
||||||
|
config_entry_id = config_entry.entry_id
|
||||||
|
|
||||||
for doorstation in hass.data[DOORBIRD_DOMAIN]:
|
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
|
||||||
relays = doorstation.device.info()["RELAYS"]
|
doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO]
|
||||||
relays.append(IR_RELAY)
|
|
||||||
|
|
||||||
for relay in relays:
|
relays = doorstation_info["RELAYS"]
|
||||||
switch = DoorBirdSwitch(doorstation, relay)
|
relays.append(IR_RELAY)
|
||||||
switches.append(switch)
|
|
||||||
|
|
||||||
add_entities(switches)
|
for relay in relays:
|
||||||
|
switch = DoorBirdSwitch(doorstation, doorstation_info, relay)
|
||||||
|
entities.append(switch)
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class DoorBirdSwitch(SwitchDevice):
|
class DoorBirdSwitch(DoorBirdEntity, SwitchDevice):
|
||||||
"""A relay in a DoorBird device."""
|
"""A relay in a DoorBird device."""
|
||||||
|
|
||||||
def __init__(self, doorstation, relay):
|
def __init__(self, doorstation, doorstation_info, relay):
|
||||||
"""Initialize a relay in a DoorBird device."""
|
"""Initialize a relay in a DoorBird device."""
|
||||||
|
super().__init__(doorstation, doorstation_info)
|
||||||
self._doorstation = doorstation
|
self._doorstation = doorstation
|
||||||
self._relay = relay
|
self._relay = relay
|
||||||
self._state = False
|
self._state = False
|
||||||
@ -41,6 +46,12 @@ class DoorBirdSwitch(SwitchDevice):
|
|||||||
self._time = datetime.timedelta(minutes=5)
|
self._time = datetime.timedelta(minutes=5)
|
||||||
else:
|
else:
|
||||||
self._time = datetime.timedelta(seconds=5)
|
self._time = datetime.timedelta(seconds=5)
|
||||||
|
self._unique_id = f"{self._mac_addr}_{self._relay}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Switch unique id."""
|
||||||
|
return self._unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
19
homeassistant/components/doorbird/util.py
Normal file
19
homeassistant/components/doorbird/util.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"""DoorBird integration utils."""
|
||||||
|
|
||||||
|
from .const import DOMAIN, DOOR_STATION
|
||||||
|
|
||||||
|
|
||||||
|
def get_mac_address_from_doorstation_info(doorstation_info):
|
||||||
|
"""Get the mac address depending on the device type."""
|
||||||
|
if "PRIMARY_MAC_ADDR" in doorstation_info:
|
||||||
|
return doorstation_info["PRIMARY_MAC_ADDR"]
|
||||||
|
return doorstation_info["WIFI_MAC_ADDR"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_doorstation_by_token(hass, token):
|
||||||
|
"""Get doorstation by slug."""
|
||||||
|
for config_entry_id in hass.data[DOMAIN]:
|
||||||
|
doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION]
|
||||||
|
|
||||||
|
if token == doorstation.token:
|
||||||
|
return doorstation
|
@ -24,6 +24,7 @@ FLOWS = [
|
|||||||
"deconz",
|
"deconz",
|
||||||
"dialogflow",
|
"dialogflow",
|
||||||
"directv",
|
"directv",
|
||||||
|
"doorbird",
|
||||||
"dynalite",
|
"dynalite",
|
||||||
"ecobee",
|
"ecobee",
|
||||||
"elgato",
|
"elgato",
|
||||||
|
@ -7,7 +7,8 @@ To update, run python3 -m script.hassfest
|
|||||||
|
|
||||||
ZEROCONF = {
|
ZEROCONF = {
|
||||||
"_axis-video._tcp.local.": [
|
"_axis-video._tcp.local.": [
|
||||||
"axis"
|
"axis",
|
||||||
|
"doorbird"
|
||||||
],
|
],
|
||||||
"_coap._udp.local.": [
|
"_coap._udp.local.": [
|
||||||
"tradfri"
|
"tradfri"
|
||||||
|
@ -179,6 +179,9 @@ directpy==0.7
|
|||||||
# homeassistant.components.updater
|
# homeassistant.components.updater
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.doorbird
|
||||||
|
doorbirdpy==2.0.8
|
||||||
|
|
||||||
# homeassistant.components.dsmr
|
# homeassistant.components.dsmr
|
||||||
dsmr_parser==0.18
|
dsmr_parser==0.18
|
||||||
|
|
||||||
|
1
tests/components/doorbird/__init__.py
Normal file
1
tests/components/doorbird/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the DoorBird integration."""
|
258
tests/components/doorbird/test_config_flow.py
Normal file
258
tests/components/doorbird/test_config_flow.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
"""Test the DoorBird config flow."""
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
from asynctest import MagicMock, patch
|
||||||
|
|
||||||
|
from homeassistant import config_entries, data_entry_flow, setup
|
||||||
|
from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN
|
||||||
|
from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, init_recorder_component
|
||||||
|
|
||||||
|
VALID_CONFIG = {
|
||||||
|
CONF_HOST: "1.2.3.4",
|
||||||
|
CONF_USERNAME: "friend",
|
||||||
|
CONF_PASSWORD: "password",
|
||||||
|
CONF_NAME: "mydoorbird",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mock_doorbirdapi_return_values(ready=None, info=None):
|
||||||
|
doorbirdapi_mock = MagicMock()
|
||||||
|
type(doorbirdapi_mock).ready = MagicMock(return_value=ready)
|
||||||
|
type(doorbirdapi_mock).info = MagicMock(return_value=info)
|
||||||
|
|
||||||
|
return doorbirdapi_mock
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mock_doorbirdapi_side_effects(ready=None, info=None):
|
||||||
|
doorbirdapi_mock = MagicMock()
|
||||||
|
type(doorbirdapi_mock).ready = MagicMock(side_effect=ready)
|
||||||
|
type(doorbirdapi_mock).info = MagicMock(side_effect=info)
|
||||||
|
|
||||||
|
return doorbirdapi_mock
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form(hass):
|
||||||
|
"""Test we get the user form."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||||
|
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], VALID_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "1.2.3.4"
|
||||||
|
assert result2["data"] == {
|
||||||
|
"host": "1.2.3.4",
|
||||||
|
"name": "mydoorbird",
|
||||||
|
"password": "password",
|
||||||
|
"username": "friend",
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_import(hass):
|
||||||
|
"""Test we get the form with import source."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
import_config = VALID_CONFIG.copy()
|
||||||
|
import_config[CONF_EVENTS] = ["event1", "event2", "event3"]
|
||||||
|
import_config[CONF_TOKEN] = "imported_token"
|
||||||
|
import_config[
|
||||||
|
CONF_CUSTOM_URL
|
||||||
|
] = "http://legacy.custom.url/should/only/come/in/from/yaml"
|
||||||
|
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||||
|
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
), patch("homeassistant.components.logbook.async_setup", return_value=True), patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
|
data=import_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == "create_entry"
|
||||||
|
assert result["title"] == "1.2.3.4"
|
||||||
|
assert result["data"] == {
|
||||||
|
"host": "1.2.3.4",
|
||||||
|
"name": "mydoorbird",
|
||||||
|
"password": "password",
|
||||||
|
"username": "friend",
|
||||||
|
"events": ["event1", "event2", "event3"],
|
||||||
|
"token": "imported_token",
|
||||||
|
# This will go away once we convert to cloud hooks
|
||||||
|
"hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml",
|
||||||
|
}
|
||||||
|
# It is not possible to import options at this time
|
||||||
|
# so they end up in the config entry data and are
|
||||||
|
# used a fallback when they are not in options
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_zeroconf_wrong_oui(hass):
|
||||||
|
"""Test we abort when we get the wrong OUI via zeroconf."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data={
|
||||||
|
"properties": {"macaddress": "notdoorbirdoui"},
|
||||||
|
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_zeroconf_correct_oui(hass):
|
||||||
|
"""Test we can setup from zeroconf with the correct OUI source."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||||
|
data={
|
||||||
|
"properties": {"macaddress": "1CCAE3DOORBIRD"},
|
||||||
|
"name": "Doorstation - abc123._axis-video._tcp.local.",
|
||||||
|
"host": "192.168.1.5",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_return_values(
|
||||||
|
ready=[True], info={"WIFI_MAC_ADDR": "macaddr"}
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
), patch("homeassistant.components.logbook.async_setup", return_value=True), patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], VALID_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == "1.2.3.4"
|
||||||
|
assert result2["data"] == {
|
||||||
|
"host": "1.2.3.4",
|
||||||
|
"name": "mydoorbird",
|
||||||
|
"password": "password",
|
||||||
|
"username": "friend",
|
||||||
|
}
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=OSError)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], VALID_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_user_invalid_auth(hass):
|
||||||
|
"""Test we handle cannot invalid auth error."""
|
||||||
|
await hass.async_add_job(init_recorder_component, hass) # force in memory db
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_urllib_error = urllib.error.HTTPError(
|
||||||
|
"http://xyz.tld", 401, "login failed", {}, None
|
||||||
|
)
|
||||||
|
doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.config_flow.DoorBird",
|
||||||
|
return_value=doorbirdapi,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], VALID_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow(hass):
|
||||||
|
"""Test config flow options."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
unique_id="abcde12345",
|
||||||
|
data=VALID_CONFIG,
|
||||||
|
options={CONF_EVENTS: ["event1", "event2", "event3"]},
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.doorbird.async_setup_entry", return_value=True
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"], user_input={CONF_EVENTS: "eventa, eventc, eventq"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]}
|
@ -62,7 +62,10 @@ async def test_setup(hass, mock_zeroconf):
|
|||||||
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
|
||||||
|
|
||||||
assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF)
|
assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF)
|
||||||
assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2
|
expected_flow_calls = 0
|
||||||
|
for matching_components in zc_gen.ZEROCONF.values():
|
||||||
|
expected_flow_calls += len(matching_components)
|
||||||
|
assert len(mock_config_flow.mock_calls) == expected_flow_calls * 2
|
||||||
|
|
||||||
|
|
||||||
async def test_homekit_match_partial(hass, mock_zeroconf):
|
async def test_homekit_match_partial(hass, mock_zeroconf):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user