1 """Reolink integration for HomeAssistant."""
3 from __future__
import annotations
6 from datetime
import timedelta
9 from reolink_aio.api
import RETRY_ATTEMPTS
10 from reolink_aio.exceptions
import CredentialsInvalidError, ReolinkError
17 config_validation
as cv,
18 device_registry
as dr,
19 entity_registry
as er,
25 from .const
import CONF_USE_HTTPS, DOMAIN
26 from .exceptions
import PasswordIncompatible, ReolinkException, UserNotAdmin
27 from .host
import ReolinkHost
28 from .services
import async_setup_services
29 from .util
import ReolinkConfigEntry, ReolinkData, get_device_uid_and_ch
31 _LOGGER = logging.getLogger(__name__)
34 Platform.BINARY_SENSOR,
49 CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
52 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
53 """Set up Reolink shared code."""
60 hass: HomeAssistant, config_entry: ReolinkConfigEntry
62 """Set up Reolink from a config entry."""
63 host =
ReolinkHost(hass, config_entry.data, config_entry.options)
66 await host.async_init()
67 except (UserNotAdmin, CredentialsInvalidError, PasswordIncompatible)
as err:
76 f
"Error while trying to setup {host.api.host}:{host.api.port}: {err!s}"
82 config_entry.async_on_unload(
83 hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, host.stop)
88 host.api.port != config_entry.data[CONF_PORT]
89 or host.api.use_https != config_entry.data[CONF_USE_HTTPS]
92 "HTTP(s) port of Reolink %s, changed from %s to %s",
94 config_entry.data[CONF_PORT],
99 CONF_PORT: host.api.port,
100 CONF_USE_HTTPS: host.api.use_https,
102 hass.config_entries.async_update_entry(config_entry, data=data)
104 async
def async_device_config_update() -> None:
105 """Update the host state cache and renew the ONVIF-subscription."""
106 async
with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
108 await host.update_states()
109 except CredentialsInvalidError
as err:
110 host.credential_errors += 1
111 if host.credential_errors >= NUM_CRED_ERRORS:
115 except ReolinkError
as err:
116 host.credential_errors = 0
119 host.credential_errors = 0
121 async
with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
124 if host.api.new_devices
and config_entry.state == ConfigEntryState.LOADED:
126 hass.async_create_task(
127 hass.config_entries.async_reload(config_entry.entry_id)
130 async
def async_check_firmware_update() -> None:
131 """Check for firmware updates."""
132 async
with asyncio.timeout(host.api.timeout * (RETRY_ATTEMPTS + 2)):
134 await host.api.check_new_firmware(host.firmware_ch_list)
135 except ReolinkError
as err:
138 "Error checking Reolink firmware update at startup "
139 "from %s, possibly internet access is blocked",
145 f
"Error checking Reolink firmware update from {host.api.nvr_name}, "
146 "if the camera is blocked from accessing the internet, "
147 "disable the update entity"
150 host.starting =
False
155 config_entry=config_entry,
156 name=f
"reolink.{host.api.nvr_name}",
157 update_method=async_device_config_update,
158 update_interval=DEVICE_UPDATE_INTERVAL,
163 config_entry=config_entry,
164 name=f
"reolink.{host.api.nvr_name}.firmware",
165 update_method=async_check_firmware_update,
166 update_interval=FIRMWARE_UPDATE_INTERVAL,
170 config_entry.async_create_background_task(
172 firmware_coordinator.async_refresh(),
173 f
"Reolink firmware check {config_entry.entry_id}",
177 await device_coordinator.async_config_entry_first_refresh()
178 except BaseException:
184 device_coordinator=device_coordinator,
185 firmware_coordinator=firmware_coordinator,
190 await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
192 config_entry.async_on_unload(
193 config_entry.add_update_listener(entry_update_listener)
200 hass: HomeAssistant, config_entry: ReolinkConfigEntry
202 """Update the configuration of the host entity."""
203 await hass.config_entries.async_reload(config_entry.entry_id)
207 hass: HomeAssistant, config_entry: ReolinkConfigEntry
209 """Unload a config entry."""
210 host: ReolinkHost = config_entry.runtime_data.host
214 return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
218 hass: HomeAssistant, config_entry: ReolinkConfigEntry, device: dr.DeviceEntry
220 """Remove a device from a config entry."""
221 host: ReolinkHost = config_entry.runtime_data.host
225 await host.api.get_state(cmd=
"GetDingDongList")
226 chime = host.api.chime(ch)
229 or chime.connect_state
is None
230 or chime.connect_state < 0
231 or chime.channel
not in host.api.channels
234 "Removing Reolink chime %s with id %s, "
235 "since it is not coupled to %s anymore",
244 await host.api.get_state(cmd=
"GetDingDongList")
245 if chime.connect_state < 0:
247 "Removed Reolink chime %s with id %s from %s",
255 "Cannot remove Reolink chime %s with id %s, because it is still connected "
256 "to %s, please first remove the chime "
257 "in the reolink app",
264 if not host.api.is_nvr
or ch
is None:
266 "Cannot remove Reolink device %s, because it is not a camera connected "
267 "to a NVR/Hub, please remove the integration entry instead",
272 if ch
not in host.api.channels:
274 "Removing Reolink device %s, "
275 "since no camera is connected to NVR channel %s anymore",
281 await host.api.get_state(cmd=
"GetChannelstatus")
282 if not host.api.camera_online(ch):
284 "Removing Reolink device %s, "
285 "since the camera connected to channel %s is offline",
292 "Cannot remove Reolink device %s on channel %s, because it is still connected "
293 "to the NVR/Hub, please first remove the camera from the NVR/Hub "
294 "in the reolink app",
302 hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
304 """Migrate entity IDs if needed."""
305 device_reg = dr.async_get(hass)
306 devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
308 for device
in devices:
311 if host.api.supported(
None,
"UID")
and device_uid[0] != host.unique_id:
313 new_device_id = f
"{host.unique_id}"
315 new_device_id = f
"{host.unique_id}_{device_uid[1]}"
316 new_identifiers = {(DOMAIN, new_device_id)}
317 device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
319 if ch
is None or is_chime:
322 ch_device_ids[device.id] = ch
323 if host.api.supported(ch,
"UID")
and device_uid[1] != host.api.camera_uid(ch):
324 if host.api.supported(
None,
"UID"):
325 new_device_id = f
"{host.unique_id}_{host.api.camera_uid(ch)}"
327 new_device_id = f
"{device_uid[0]}_{host.api.camera_uid(ch)}"
328 new_identifiers = {(DOMAIN, new_device_id)}
329 existing_device = device_reg.async_get_device(identifiers=new_identifiers)
330 if existing_device
is None:
331 device_reg.async_update_device(
332 device.id, new_identifiers=new_identifiers
336 "Reolink device with uid %s already exists, "
337 "removing device with uid %s",
341 device_reg.async_remove_device(device.id)
343 entity_reg = er.async_get(hass)
344 entities = er.async_entries_for_config_entry(entity_reg, config_entry_id)
345 for entity
in entities:
347 if entity.domain ==
"update" and entity.unique_id
in [
351 entity_reg.async_update_entity(
352 entity.entity_id, new_unique_id=f
"{host.unique_id}_firmware"
356 if host.api.supported(
None,
"UID")
and not entity.unique_id.startswith(
359 new_id = f
"{host.unique_id}_{entity.unique_id.split("_
", 1)[1]}"
360 entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
362 if entity.device_id
in ch_device_ids:
363 ch = ch_device_ids[entity.device_id]
364 id_parts = entity.unique_id.split(
"_", 2)
365 if host.api.supported(ch,
"UID")
and id_parts[1] != host.api.camera_uid(ch):
366 new_id = f
"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}"
367 existing_entity = entity_reg.async_get_entity_id(
368 entity.domain, entity.platform, new_id
370 if existing_entity
is None:
371 entity_reg.async_update_entity(
372 entity.entity_id, new_unique_id=new_id
376 "Reolink entity with unique_id %s already exists, "
377 "removing device with unique_id %s",
381 entity_reg.async_remove(entity.entity_id)
None async_setup_services(HomeAssistant hass)
tuple[list[str], int|None, bool] get_device_uid_and_ch(dr.DeviceEntry device, ReolinkHost host)
bool async_remove_config_entry_device(HomeAssistant hass, ReolinkConfigEntry config_entry, dr.DeviceEntry device)
bool async_unload_entry(HomeAssistant hass, ReolinkConfigEntry config_entry)
None entry_update_listener(HomeAssistant hass, ReolinkConfigEntry config_entry)
bool async_setup(HomeAssistant hass, ConfigType config)
bool async_setup_entry(HomeAssistant hass, ReolinkConfigEntry config_entry)
None migrate_entity_ids(HomeAssistant hass, str config_entry_id, ReolinkHost host)