Revert "Add ability to ignore devices for UniFi Protect" (#77916)
This commit is contained in:
parent
5632e33426
commit
dbb556a812
12 changed files with 75 additions and 258 deletions
|
@ -26,7 +26,6 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALL_UPDATES,
|
CONF_ALL_UPDATES,
|
||||||
CONF_IGNORED,
|
|
||||||
CONF_OVERRIDE_CHOST,
|
CONF_OVERRIDE_CHOST,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEVICES_FOR_SUBSCRIBE,
|
DEVICES_FOR_SUBSCRIBE,
|
||||||
|
@ -36,11 +35,11 @@ from .const import (
|
||||||
OUTDATED_LOG_MESSAGE,
|
OUTDATED_LOG_MESSAGE,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
)
|
)
|
||||||
from .data import ProtectData
|
from .data import ProtectData, async_ufp_instance_for_config_entry_ids
|
||||||
from .discovery import async_start_discovery
|
from .discovery import async_start_discovery
|
||||||
from .migrate import async_migrate_data
|
from .migrate import async_migrate_data
|
||||||
from .services import async_cleanup_services, async_setup_services
|
from .services import async_cleanup_services, async_setup_services
|
||||||
from .utils import async_unifi_mac, convert_mac_list
|
from .utils import _async_unifi_mac_from_hass, async_get_devices
|
||||||
from .views import ThumbnailProxyView, VideoProxyView
|
from .views import ThumbnailProxyView, VideoProxyView
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -107,19 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
|
||||||
async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Update options."""
|
"""Update options."""
|
||||||
|
|
||||||
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
changed = data.async_get_changed_options(entry)
|
|
||||||
|
|
||||||
if len(changed) == 1 and CONF_IGNORED in changed:
|
|
||||||
new_macs = convert_mac_list(entry.options.get(CONF_IGNORED, ""))
|
|
||||||
added_macs = new_macs - data.ignored_macs
|
|
||||||
removed_macs = data.ignored_macs - new_macs
|
|
||||||
# if only ignored macs are added, we can handle without reloading
|
|
||||||
if not removed_macs and added_macs:
|
|
||||||
data.async_add_new_ignored_macs(added_macs)
|
|
||||||
return
|
|
||||||
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@ -139,15 +125,15 @@ async def async_remove_config_entry_device(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Remove ufp config entry from a device."""
|
"""Remove ufp config entry from a device."""
|
||||||
unifi_macs = {
|
unifi_macs = {
|
||||||
async_unifi_mac(connection[1])
|
_async_unifi_mac_from_hass(connection[1])
|
||||||
for connection in device_entry.connections
|
for connection in device_entry.connections
|
||||||
if connection[0] == dr.CONNECTION_NETWORK_MAC
|
if connection[0] == dr.CONNECTION_NETWORK_MAC
|
||||||
}
|
}
|
||||||
data: ProtectData = hass.data[DOMAIN][config_entry.entry_id]
|
api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id})
|
||||||
if data.api.bootstrap.nvr.mac in unifi_macs:
|
assert api is not None
|
||||||
|
if api.bootstrap.nvr.mac in unifi_macs:
|
||||||
return False
|
return False
|
||||||
for device in data.get_by_types(DEVICES_THAT_ADOPT):
|
for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT):
|
||||||
if device.is_adopted_by_us and device.mac in unifi_macs:
|
if device.is_adopted_by_us and device.mac in unifi_macs:
|
||||||
data.async_ignore_mac(device.mac)
|
return False
|
||||||
break
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -35,7 +35,6 @@ from homeassistant.util.network import is_ip_address
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ALL_UPDATES,
|
CONF_ALL_UPDATES,
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
CONF_IGNORED,
|
|
||||||
CONF_MAX_MEDIA,
|
CONF_MAX_MEDIA,
|
||||||
CONF_OVERRIDE_CHOST,
|
CONF_OVERRIDE_CHOST,
|
||||||
DEFAULT_MAX_MEDIA,
|
DEFAULT_MAX_MEDIA,
|
||||||
|
@ -47,7 +46,7 @@ from .const import (
|
||||||
)
|
)
|
||||||
from .data import async_last_update_was_successful
|
from .data import async_last_update_was_successful
|
||||||
from .discovery import async_start_discovery
|
from .discovery import async_start_discovery
|
||||||
from .utils import _async_resolve, async_short_mac, async_unifi_mac, convert_mac_list
|
from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -121,7 +120,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Handle integration discovery."""
|
"""Handle integration discovery."""
|
||||||
self._discovered_device = discovery_info
|
self._discovered_device = discovery_info
|
||||||
mac = async_unifi_mac(discovery_info["hw_addr"])
|
mac = _async_unifi_mac_from_hass(discovery_info["hw_addr"])
|
||||||
await self.async_set_unique_id(mac)
|
await self.async_set_unique_id(mac)
|
||||||
source_ip = discovery_info["source_ip"]
|
source_ip = discovery_info["source_ip"]
|
||||||
direct_connect_domain = discovery_info["direct_connect_domain"]
|
direct_connect_domain = discovery_info["direct_connect_domain"]
|
||||||
|
@ -183,7 +182,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
placeholders = {
|
placeholders = {
|
||||||
"name": discovery_info["hostname"]
|
"name": discovery_info["hostname"]
|
||||||
or discovery_info["platform"]
|
or discovery_info["platform"]
|
||||||
or f"NVR {async_short_mac(discovery_info['hw_addr'])}",
|
or f"NVR {_async_short_mac(discovery_info['hw_addr'])}",
|
||||||
"ip_address": discovery_info["source_ip"],
|
"ip_address": discovery_info["source_ip"],
|
||||||
}
|
}
|
||||||
self.context["title_placeholders"] = placeholders
|
self.context["title_placeholders"] = placeholders
|
||||||
|
@ -225,7 +224,6 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
CONF_ALL_UPDATES: False,
|
CONF_ALL_UPDATES: False,
|
||||||
CONF_OVERRIDE_CHOST: False,
|
CONF_OVERRIDE_CHOST: False,
|
||||||
CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA,
|
CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA,
|
||||||
CONF_IGNORED: "",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -367,53 +365,33 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> FlowResult:
|
) -> FlowResult:
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
|
|
||||||
values = user_input or self.config_entry.options
|
|
||||||
schema = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_DISABLE_RTSP,
|
|
||||||
description={
|
|
||||||
"suggested_value": values.get(CONF_DISABLE_RTSP, False)
|
|
||||||
},
|
|
||||||
): bool,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_ALL_UPDATES,
|
|
||||||
description={
|
|
||||||
"suggested_value": values.get(CONF_ALL_UPDATES, False)
|
|
||||||
},
|
|
||||||
): bool,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_OVERRIDE_CHOST,
|
|
||||||
description={
|
|
||||||
"suggested_value": values.get(CONF_OVERRIDE_CHOST, False)
|
|
||||||
},
|
|
||||||
): bool,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_MAX_MEDIA,
|
|
||||||
description={
|
|
||||||
"suggested_value": values.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
|
|
||||||
},
|
|
||||||
): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_IGNORED,
|
|
||||||
description={"suggested_value": values.get(CONF_IGNORED, "")},
|
|
||||||
): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
return self.async_create_entry(title="", data=user_input)
|
||||||
convert_mac_list(user_input.get(CONF_IGNORED, ""), raise_exception=True)
|
|
||||||
except vol.Invalid:
|
|
||||||
errors[CONF_IGNORED] = "invalid_mac_list"
|
|
||||||
|
|
||||||
if not errors:
|
|
||||||
return self.async_create_entry(title="", data=user_input)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=schema,
|
data_schema=vol.Schema(
|
||||||
errors=errors,
|
{
|
||||||
|
vol.Optional(
|
||||||
|
CONF_DISABLE_RTSP,
|
||||||
|
default=self.config_entry.options.get(CONF_DISABLE_RTSP, False),
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_ALL_UPDATES,
|
||||||
|
default=self.config_entry.options.get(CONF_ALL_UPDATES, False),
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_OVERRIDE_CHOST,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_OVERRIDE_CHOST, False
|
||||||
|
),
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_MEDIA,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA
|
||||||
|
),
|
||||||
|
): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)),
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,7 +20,6 @@ CONF_DISABLE_RTSP = "disable_rtsp"
|
||||||
CONF_ALL_UPDATES = "all_updates"
|
CONF_ALL_UPDATES = "all_updates"
|
||||||
CONF_OVERRIDE_CHOST = "override_connection_host"
|
CONF_OVERRIDE_CHOST = "override_connection_host"
|
||||||
CONF_MAX_MEDIA = "max_media"
|
CONF_MAX_MEDIA = "max_media"
|
||||||
CONF_IGNORED = "ignored_devices"
|
|
||||||
|
|
||||||
CONFIG_OPTIONS = [
|
CONFIG_OPTIONS = [
|
||||||
CONF_ALL_UPDATES,
|
CONF_ALL_UPDATES,
|
||||||
|
|
|
@ -28,7 +28,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
CONF_IGNORED,
|
|
||||||
CONF_MAX_MEDIA,
|
CONF_MAX_MEDIA,
|
||||||
DEFAULT_MAX_MEDIA,
|
DEFAULT_MAX_MEDIA,
|
||||||
DEVICES_THAT_ADOPT,
|
DEVICES_THAT_ADOPT,
|
||||||
|
@ -37,11 +36,7 @@ from .const import (
|
||||||
DISPATCH_CHANNELS,
|
DISPATCH_CHANNELS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type
|
||||||
async_dispatch_id as _ufpd,
|
|
||||||
async_get_devices_by_type,
|
|
||||||
convert_mac_list,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR]
|
ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR]
|
||||||
|
@ -72,7 +67,6 @@ class ProtectData:
|
||||||
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
self._existing_options = dict(entry.options)
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._update_interval = update_interval
|
self._update_interval = update_interval
|
||||||
self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {}
|
self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {}
|
||||||
|
@ -80,8 +74,6 @@ class ProtectData:
|
||||||
self._unsub_interval: CALLBACK_TYPE | None = None
|
self._unsub_interval: CALLBACK_TYPE | None = None
|
||||||
self._unsub_websocket: CALLBACK_TYPE | None = None
|
self._unsub_websocket: CALLBACK_TYPE | None = None
|
||||||
self._auth_failures = 0
|
self._auth_failures = 0
|
||||||
self._ignored_macs: set[str] | None = None
|
|
||||||
self._ignore_update_cancel: Callable[[], None] | None = None
|
|
||||||
|
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
self.api = protect
|
self.api = protect
|
||||||
|
@ -96,47 +88,6 @@ class ProtectData:
|
||||||
"""Max number of events to load at once."""
|
"""Max number of events to load at once."""
|
||||||
return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
|
return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA)
|
||||||
|
|
||||||
@property
|
|
||||||
def ignored_macs(self) -> set[str]:
|
|
||||||
"""Set of ignored MAC addresses."""
|
|
||||||
|
|
||||||
if self._ignored_macs is None:
|
|
||||||
self._ignored_macs = convert_mac_list(
|
|
||||||
self._entry.options.get(CONF_IGNORED, "")
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._ignored_macs
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_get_changed_options(self, entry: ConfigEntry) -> dict[str, Any]:
|
|
||||||
"""Get changed options for when entry is updated."""
|
|
||||||
|
|
||||||
return dict(
|
|
||||||
set(self._entry.options.items()) - set(self._existing_options.items())
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_ignore_mac(self, mac: str) -> None:
|
|
||||||
"""Ignores a MAC address for a UniFi Protect device."""
|
|
||||||
|
|
||||||
new_macs = (self._ignored_macs or set()).copy()
|
|
||||||
new_macs.add(mac)
|
|
||||||
_LOGGER.debug("Updating ignored_devices option: %s", self.ignored_macs)
|
|
||||||
options = dict(self._entry.options)
|
|
||||||
options[CONF_IGNORED] = ",".join(new_macs)
|
|
||||||
self._hass.config_entries.async_update_entry(self._entry, options=options)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_add_new_ignored_macs(self, new_macs: set[str]) -> None:
|
|
||||||
"""Add new ignored MAC addresses and ensures the devices are removed."""
|
|
||||||
|
|
||||||
for mac in new_macs:
|
|
||||||
device = self.api.bootstrap.get_device_from_mac(mac)
|
|
||||||
if device is not None:
|
|
||||||
self._async_remove_device(device)
|
|
||||||
self._ignored_macs = None
|
|
||||||
self._existing_options = dict(self._entry.options)
|
|
||||||
|
|
||||||
def get_by_types(
|
def get_by_types(
|
||||||
self, device_types: Iterable[ModelType], ignore_unadopted: bool = True
|
self, device_types: Iterable[ModelType], ignore_unadopted: bool = True
|
||||||
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
||||||
|
@ -148,8 +99,6 @@ class ProtectData:
|
||||||
for device in devices:
|
for device in devices:
|
||||||
if ignore_unadopted and not device.is_adopted_by_us:
|
if ignore_unadopted and not device.is_adopted_by_us:
|
||||||
continue
|
continue
|
||||||
if device.mac in self.ignored_macs:
|
|
||||||
continue
|
|
||||||
yield device
|
yield device
|
||||||
|
|
||||||
async def async_setup(self) -> None:
|
async def async_setup(self) -> None:
|
||||||
|
@ -159,11 +108,6 @@ class ProtectData:
|
||||||
)
|
)
|
||||||
await self.async_refresh()
|
await self.async_refresh()
|
||||||
|
|
||||||
for mac in self.ignored_macs:
|
|
||||||
device = self.api.bootstrap.get_device_from_mac(mac)
|
|
||||||
if device is not None:
|
|
||||||
self._async_remove_device(device)
|
|
||||||
|
|
||||||
async def async_stop(self, *args: Any) -> None:
|
async def async_stop(self, *args: Any) -> None:
|
||||||
"""Stop processing data."""
|
"""Stop processing data."""
|
||||||
if self._unsub_websocket:
|
if self._unsub_websocket:
|
||||||
|
@ -228,7 +172,6 @@ class ProtectData:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None:
|
def _async_remove_device(self, device: ProtectAdoptableDeviceModel) -> None:
|
||||||
|
|
||||||
registry = dr.async_get(self._hass)
|
registry = dr.async_get(self._hass)
|
||||||
device_entry = registry.async_get_device(
|
device_entry = registry.async_get_device(
|
||||||
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
|
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
|
||||||
|
@ -353,13 +296,13 @@ class ProtectData:
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_ufp_data_for_config_entry_ids(
|
def async_ufp_instance_for_config_entry_ids(
|
||||||
hass: HomeAssistant, config_entry_ids: set[str]
|
hass: HomeAssistant, config_entry_ids: set[str]
|
||||||
) -> ProtectData | None:
|
) -> ProtectApiClient | None:
|
||||||
"""Find the UFP instance for the config entry ids."""
|
"""Find the UFP instance for the config entry ids."""
|
||||||
domain_data = hass.data[DOMAIN]
|
domain_data = hass.data[DOMAIN]
|
||||||
for config_entry_id in config_entry_ids:
|
for config_entry_id in config_entry_ids:
|
||||||
if config_entry_id in domain_data:
|
if config_entry_id in domain_data:
|
||||||
protect_data: ProtectData = domain_data[config_entry_id]
|
protect_data: ProtectData = domain_data[config_entry_id]
|
||||||
return protect_data
|
return protect_data.api
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -25,7 +25,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids
|
||||||
from homeassistant.util.read_only_dict import ReadOnlyDict
|
from homeassistant.util.read_only_dict import ReadOnlyDict
|
||||||
|
|
||||||
from .const import ATTR_MESSAGE, DOMAIN
|
from .const import ATTR_MESSAGE, DOMAIN
|
||||||
from .data import async_ufp_data_for_config_entry_ids
|
from .data import async_ufp_instance_for_config_entry_ids
|
||||||
|
|
||||||
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
|
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
|
||||||
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
|
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
|
||||||
|
@ -70,8 +70,8 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl
|
||||||
return _async_get_ufp_instance(hass, device_entry.via_device_id)
|
return _async_get_ufp_instance(hass, device_entry.via_device_id)
|
||||||
|
|
||||||
config_entry_ids = device_entry.config_entries
|
config_entry_ids = device_entry.config_entries
|
||||||
if ufp_data := async_ufp_data_for_config_entry_ids(hass, config_entry_ids):
|
if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids):
|
||||||
return ufp_data.api
|
return ufp_instance
|
||||||
|
|
||||||
raise HomeAssistantError(f"No device found for device id: {device_id}")
|
raise HomeAssistantError(f"No device found for device id: {device_id}")
|
||||||
|
|
||||||
|
|
|
@ -50,13 +50,9 @@
|
||||||
"disable_rtsp": "Disable the RTSP stream",
|
"disable_rtsp": "Disable the RTSP stream",
|
||||||
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
|
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
|
||||||
"override_connection_host": "Override Connection Host",
|
"override_connection_host": "Override Connection Host",
|
||||||
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
|
"max_media": "Max number of event to load for Media Browser (increases RAM usage)"
|
||||||
"ignored_devices": "Comma separated list of MAC addresses of devices to ignore"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"invalid_mac_list": "Must be a list of MAC addresses seperated by commas"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,15 +42,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"error": {
|
|
||||||
"invalid_mac_list": "Must be a list of MAC addresses seperated by commas"
|
|
||||||
},
|
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init": {
|
||||||
"data": {
|
"data": {
|
||||||
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
|
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
|
||||||
"disable_rtsp": "Disable the RTSP stream",
|
"disable_rtsp": "Disable the RTSP stream",
|
||||||
"ignored_devices": "Comma separated list of MAC addresses of devices to ignore",
|
|
||||||
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
|
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
|
||||||
"override_connection_host": "Override Connection Host"
|
"override_connection_host": "Override Connection Host"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
"""UniFi Protect Integration utils."""
|
"""UniFi Protect Integration utils."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator, Iterable
|
||||||
import contextlib
|
import contextlib
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import re
|
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
@ -14,16 +14,12 @@ from pyunifiprotect.data import (
|
||||||
LightModeType,
|
LightModeType,
|
||||||
ProtectAdoptableDeviceModel,
|
ProtectAdoptableDeviceModel,
|
||||||
)
|
)
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
|
|
||||||
from .const import DOMAIN, ModelType
|
from .const import DOMAIN, ModelType
|
||||||
|
|
||||||
MAC_RE = re.compile(r"[0-9A-F]{12}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_nested_attr(obj: Any, attr: str) -> Any:
|
def get_nested_attr(obj: Any, attr: str) -> Any:
|
||||||
"""Fetch a nested attribute."""
|
"""Fetch a nested attribute."""
|
||||||
|
@ -42,16 +38,15 @@ def get_nested_attr(obj: Any, attr: str) -> Any:
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_unifi_mac(mac: str) -> str:
|
def _async_unifi_mac_from_hass(mac: str) -> str:
|
||||||
"""Convert MAC address to format from UniFi Protect."""
|
|
||||||
# MAC addresses in UFP are always caps
|
# MAC addresses in UFP are always caps
|
||||||
return mac.replace(":", "").replace("-", "").replace("_", "").upper()
|
return mac.replace(":", "").upper()
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_short_mac(mac: str) -> str:
|
def _async_short_mac(mac: str) -> str:
|
||||||
"""Get the short mac address from the full mac."""
|
"""Get the short mac address from the full mac."""
|
||||||
return async_unifi_mac(mac)[-6:]
|
return _async_unifi_mac_from_hass(mac)[-6:]
|
||||||
|
|
||||||
|
|
||||||
async def _async_resolve(hass: HomeAssistant, host: str) -> str | None:
|
async def _async_resolve(hass: HomeAssistant, host: str) -> str | None:
|
||||||
|
@ -82,6 +77,18 @@ def async_get_devices_by_type(
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_devices(
|
||||||
|
bootstrap: Bootstrap, model_type: Iterable[ModelType]
|
||||||
|
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
|
||||||
|
"""Return all device by type."""
|
||||||
|
return (
|
||||||
|
device
|
||||||
|
for device_type in model_type
|
||||||
|
for device in async_get_devices_by_type(bootstrap, device_type).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_get_light_motion_current(obj: Light) -> str:
|
def async_get_light_motion_current(obj: Light) -> str:
|
||||||
"""Get light motion mode for Flood Light."""
|
"""Get light motion mode for Flood Light."""
|
||||||
|
@ -99,22 +106,3 @@ def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str:
|
||||||
"""Generate entry specific dispatch ID."""
|
"""Generate entry specific dispatch ID."""
|
||||||
|
|
||||||
return f"{DOMAIN}.{entry.entry_id}.{dispatch}"
|
return f"{DOMAIN}.{entry.entry_id}.{dispatch}"
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def convert_mac_list(option: str, raise_exception: bool = False) -> set[str]:
|
|
||||||
"""Convert csv list of MAC addresses."""
|
|
||||||
|
|
||||||
macs = set()
|
|
||||||
values = cv.ensure_list_csv(option)
|
|
||||||
for value in values:
|
|
||||||
if value == "":
|
|
||||||
continue
|
|
||||||
value = async_unifi_mac(value)
|
|
||||||
if not MAC_RE.match(value):
|
|
||||||
if raise_exception:
|
|
||||||
raise vol.Invalid("invalid_mac_list")
|
|
||||||
continue
|
|
||||||
macs.add(value)
|
|
||||||
|
|
||||||
return macs
|
|
||||||
|
|
|
@ -68,7 +68,6 @@ def mock_ufp_config_entry():
|
||||||
"port": 443,
|
"port": 443,
|
||||||
"verify_ssl": False,
|
"verify_ssl": False,
|
||||||
},
|
},
|
||||||
options={"ignored_devices": "FFFFFFFFFFFF,test"},
|
|
||||||
version=2,
|
version=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ from homeassistant.components import dhcp, ssdp
|
||||||
from homeassistant.components.unifiprotect.const import (
|
from homeassistant.components.unifiprotect.const import (
|
||||||
CONF_ALL_UPDATES,
|
CONF_ALL_UPDATES,
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
CONF_IGNORED,
|
|
||||||
CONF_OVERRIDE_CHOST,
|
CONF_OVERRIDE_CHOST,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
|
@ -270,52 +269,10 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -
|
||||||
"all_updates": True,
|
"all_updates": True,
|
||||||
"disable_rtsp": True,
|
"disable_rtsp": True,
|
||||||
"override_connection_host": True,
|
"override_connection_host": True,
|
||||||
|
"max_media": 1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_options_invalid_mac(
|
|
||||||
hass: HomeAssistant, ufp_client: ProtectApiClient
|
|
||||||
) -> None:
|
|
||||||
"""Test we handle options flows."""
|
|
||||||
mock_config = MockConfigEntry(
|
|
||||||
domain=DOMAIN,
|
|
||||||
data={
|
|
||||||
"host": "1.1.1.1",
|
|
||||||
"username": "test-username",
|
|
||||||
"password": "test-password",
|
|
||||||
"id": "UnifiProtect",
|
|
||||||
"port": 443,
|
|
||||||
"verify_ssl": False,
|
|
||||||
"max_media": 1000,
|
|
||||||
},
|
|
||||||
version=2,
|
|
||||||
unique_id=dr.format_mac(MAC_ADDR),
|
|
||||||
)
|
|
||||||
mock_config.add_to_hass(hass)
|
|
||||||
|
|
||||||
with _patch_discovery(), patch(
|
|
||||||
"homeassistant.components.unifiprotect.ProtectApiClient"
|
|
||||||
) as mock_api:
|
|
||||||
mock_api.return_value = ufp_client
|
|
||||||
|
|
||||||
await hass.config_entries.async_setup(mock_config.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert mock_config.state == config_entries.ConfigEntryState.LOADED
|
|
||||||
|
|
||||||
result = await hass.config_entries.options.async_init(mock_config.entry_id)
|
|
||||||
assert result["type"] == FlowResultType.FORM
|
|
||||||
assert not result["errors"]
|
|
||||||
assert result["step_id"] == "init"
|
|
||||||
|
|
||||||
result2 = await hass.config_entries.options.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{CONF_IGNORED: "test,test2"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] == FlowResultType.FORM
|
|
||||||
assert result2["errors"] == {CONF_IGNORED: "invalid_mac_list"}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"source, data",
|
"source, data",
|
||||||
[
|
[
|
||||||
|
|
|
@ -7,21 +7,20 @@ from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||||
from pyunifiprotect.data import NVR, Bootstrap, Doorlock, Light, Sensor
|
from pyunifiprotect.data import NVR, Bootstrap, Light
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import (
|
from homeassistant.components.unifiprotect.const import (
|
||||||
CONF_DISABLE_RTSP,
|
CONF_DISABLE_RTSP,
|
||||||
CONF_IGNORED,
|
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import _patch_discovery
|
from . import _patch_discovery
|
||||||
from .utils import MockUFPFixture, get_device_from_ufp_device, init_entry, time_changed
|
from .utils import MockUFPFixture, init_entry, time_changed
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -212,38 +211,28 @@ async def test_device_remove_devices(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
ufp: MockUFPFixture,
|
ufp: MockUFPFixture,
|
||||||
light: Light,
|
light: Light,
|
||||||
doorlock: Doorlock,
|
|
||||||
sensor: Sensor,
|
|
||||||
hass_ws_client: Callable[
|
hass_ws_client: Callable[
|
||||||
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
|
[HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse]
|
||||||
],
|
],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test we can only remove a device that no longer exists."""
|
"""Test we can only remove a device that no longer exists."""
|
||||||
|
|
||||||
sensor.mac = "FFFFFFFFFFFF"
|
await init_entry(hass, ufp, [light])
|
||||||
|
|
||||||
await init_entry(hass, ufp, [light, doorlock, sensor], regenerate_ids=False)
|
|
||||||
assert await async_setup_component(hass, "config", {})
|
assert await async_setup_component(hass, "config", {})
|
||||||
|
entity_id = "light.test_light"
|
||||||
entry_id = ufp.entry.entry_id
|
entry_id = ufp.entry.entry_id
|
||||||
|
|
||||||
|
registry: er.EntityRegistry = er.async_get(hass)
|
||||||
|
entity = registry.async_get(entity_id)
|
||||||
|
assert entity is not None
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
light_device = get_device_from_ufp_device(hass, light)
|
live_device_entry = device_registry.async_get(entity.device_id)
|
||||||
assert light_device is not None
|
|
||||||
assert (
|
assert (
|
||||||
await remove_device(await hass_ws_client(hass), light_device.id, entry_id)
|
await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id)
|
||||||
is True
|
is False
|
||||||
)
|
)
|
||||||
|
|
||||||
doorlock_device = get_device_from_ufp_device(hass, doorlock)
|
|
||||||
assert (
|
|
||||||
await remove_device(await hass_ws_client(hass), doorlock_device.id, entry_id)
|
|
||||||
is True
|
|
||||||
)
|
|
||||||
|
|
||||||
sensor_device = get_device_from_ufp_device(hass, sensor)
|
|
||||||
assert sensor_device is None
|
|
||||||
|
|
||||||
dead_device_entry = device_registry.async_get_or_create(
|
dead_device_entry = device_registry.async_get_or_create(
|
||||||
config_entry_id=entry_id,
|
config_entry_id=entry_id,
|
||||||
connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")},
|
connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")},
|
||||||
|
@ -253,10 +242,6 @@ async def test_device_remove_devices(
|
||||||
is True
|
is True
|
||||||
)
|
)
|
||||||
|
|
||||||
await time_changed(hass, 60)
|
|
||||||
entry = hass.config_entries.async_get_entry(entry_id)
|
|
||||||
entry.options[CONF_IGNORED] == f"{light.mac},{doorlock.mac}"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_device_remove_devices_nvr(
|
async def test_device_remove_devices_nvr(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
@ -23,7 +23,7 @@ from pyunifiprotect.test_util.anonymize import random_hex
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant, split_entity_id
|
from homeassistant.core import HomeAssistant, split_entity_id
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
@ -229,13 +229,3 @@ async def adopt_devices(
|
||||||
ufp.ws_msg(mock_msg)
|
ufp.ws_msg(mock_msg)
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
def get_device_from_ufp_device(
|
|
||||||
hass: HomeAssistant, device: ProtectAdoptableDeviceModel
|
|
||||||
) -> dr.DeviceEntry | None:
|
|
||||||
"""Return all device by type."""
|
|
||||||
registry = dr.async_get(hass)
|
|
||||||
return registry.async_get_device(
|
|
||||||
identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, device.mac)}
|
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue