Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ omit =
homeassistant/components/reolink/entity.py
homeassistant/components/reolink/host.py
homeassistant/components/reolink/number.py
homeassistant/components/reolink/update.py
homeassistant/components/repetier/__init__.py
homeassistant/components/repetier/sensor.py
homeassistant/components/rest/notify.py
Expand Down
35 changes: 29 additions & 6 deletions homeassistant/components/reolink/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.NUMBER]
DEVICE_UPDATE_INTERVAL = 60
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.NUMBER, Platform.UPDATE]
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12)


@dataclass
Expand All @@ -33,6 +34,7 @@ class ReolinkData:

host: ReolinkHost
device_coordinator: DataUpdateCoordinator
firmware_coordinator: DataUpdateCoordinator


async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
Expand Down Expand Up @@ -75,23 +77,44 @@ async def async_device_config_update():
async with async_timeout.timeout(host.api.timeout):
await host.renew()

coordinator_device_config_update = DataUpdateCoordinator(
async def async_check_firmware_update():
"""Check for firmware updates."""
async with async_timeout.timeout(host.api.timeout):
try:
return await host.api.check_new_firmware()
except ReolinkError as err:
raise UpdateFailed(
f"Error checking Reolink firmware update {host.api.nvr_name}"
) from err

device_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"reolink.{host.api.nvr_name}",
update_method=async_device_config_update,
update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL),
update_interval=DEVICE_UPDATE_INTERVAL,
)
firmware_coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"reolink.{host.api.nvr_name}.firmware",
update_method=async_check_firmware_update,
update_interval=FIRMWARE_UPDATE_INTERVAL,
)
# Fetch initial data so we have data when entities subscribe
try:
await coordinator_device_config_update.async_config_entry_first_refresh()
await asyncio.gather(
device_coordinator.async_config_entry_first_refresh(),
firmware_coordinator.async_config_entry_first_refresh(),
)
except ConfigEntryNotReady:
await host.stop()
raise

hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host,
device_coordinator=coordinator_device_config_update,
device_coordinator=device_coordinator,
firmware_coordinator=firmware_coordinator,
)

await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/reolink/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ async def async_setup_entry(
class ReolinkBinarySensorEntity(ReolinkCoordinatorEntity, BinarySensorEntity):
"""Base binary-sensor class for Reolink IP camera motion sensors."""

_attr_has_entity_name = True
entity_description: ReolinkBinarySensorEntityDescription

def __init__(
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/reolink/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class ReolinkCamera(ReolinkCoordinatorEntity, Camera):
"""An implementation of a Reolink IP camera."""

_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
_attr_has_entity_name = True

def __init__(
self,
Expand Down
78 changes: 53 additions & 25 deletions homeassistant/components/reolink/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,75 @@

from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from . import ReolinkData
from .const import DOMAIN


class ReolinkCoordinatorEntity(CoordinatorEntity):
"""Parent class for Reolink hardware camera entities."""
class ReolinkBaseCoordinatorEntity(CoordinatorEntity):
"""Parent class for entities that control the Reolink NVR itself, without a channel.

def __init__(self, reolink_data: ReolinkData, channel: int) -> None:
"""Initialize ReolinkCoordinatorEntity for a hardware camera."""
coordinator = reolink_data.device_coordinator
A camera connected directly to HomeAssistant without using a NVR is in the reolink API
basically a NVR with a single channel that has the camera connected to that channel.
"""

_attr_has_entity_name = True

def __init__(
self,
reolink_data: ReolinkData,
coordinator: DataUpdateCoordinator | None = None,
) -> None:
"""Initialize ReolinkBaseCoordinatorEntity for a NVR entity without a channel."""
if coordinator is None:
coordinator = reolink_data.device_coordinator
super().__init__(coordinator)

self._host = reolink_data.host
self._channel = channel

http_s = "https" if self._host.api.use_https else "http"
conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._host.unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
name=self._host.api.nvr_name,
model=self._host.api.model,
manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.hardware_version,
sw_version=self._host.api.sw_version,
configuration_url=self._conf_url,
)

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._host.api.session_active and super().available


class ReolinkCoordinatorEntity(ReolinkBaseCoordinatorEntity):
Comment thread
frenck marked this conversation as resolved.
"""Parent class for Reolink hardware camera entities connected to a channel of the NVR."""

def __init__(
self,
reolink_data: ReolinkData,
channel: int,
coordinator: DataUpdateCoordinator | None = None,
) -> None:
"""Initialize ReolinkCoordinatorEntity for a hardware camera connected to a channel of the NVR."""
super().__init__(reolink_data, coordinator)

self._channel = channel

if self._host.api.is_nvr:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")},
via_device=(DOMAIN, self._host.unique_id),
name=self._host.api.camera_name(self._channel),
model=self._host.api.camera_model(self._channel),
manufacturer=self._host.api.manufacturer,
configuration_url=conf_url,
)
else:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._host.unique_id)},
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
name=self._host.api.nvr_name,
model=self._host.api.model,
manufacturer=self._host.api.manufacturer,
hw_version=self._host.api.hardware_version,
sw_version=self._host.api.sw_version,
configuration_url=conf_url,
configuration_url=self._conf_url,
)

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._host.api.session_active and super().available
1 change: 0 additions & 1 deletion homeassistant/components/reolink/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ async def async_setup_entry(
class ReolinkNumberEntity(ReolinkCoordinatorEntity, NumberEntity):
"""Base number entity class for Reolink IP cameras."""

_attr_has_entity_name = True
entity_description: ReolinkNumberEntityDescription

def __init__(
Expand Down
78 changes: 78 additions & 0 deletions homeassistant/components/reolink/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Update entities for Reolink devices."""
from __future__ import annotations

import logging
from typing import Any

from reolink_aio.exceptions import ReolinkError

from homeassistant.components.update import (
UpdateDeviceClass,
UpdateEntity,
UpdateEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ReolinkData
from .const import DOMAIN
from .entity import ReolinkBaseCoordinatorEntity

LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up update entities for Reolink component."""
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities([ReolinkUpdateEntity(reolink_data)])


class ReolinkUpdateEntity(ReolinkBaseCoordinatorEntity, UpdateEntity):
"""Update entity for a Netgear device."""

_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_release_url = "https://reolink.com/download-center/"
_attr_name = "Update"

def __init__(
self,
reolink_data: ReolinkData,
) -> None:
"""Initialize a Netgear device."""
super().__init__(reolink_data, reolink_data.firmware_coordinator)

self._attr_unique_id = f"{self._host.unique_id}_update"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to add the platform domain to the unique_id. The entity registry is aware of both the integration domain and the platform domain. It's good to add a unique suffix if we're planning to add more than one entity type for the platform.


@property
def installed_version(self) -> str | None:
"""Version currently in use."""
return self._host.api.sw_version

@property
def latest_version(self) -> str | None:
"""Latest version available for install."""
if self.coordinator.data is None:
return None

if not self.coordinator.data:
return self.installed_version
Comment thread
frenck marked this conversation as resolved.

return self.coordinator.data

async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install the latest firmware version."""
try:
await self._host.api.update_firmware()
except ReolinkError as err:
raise HomeAssistantError(
f"Error trying to update Reolink firmware: {err}"
) from err
1 change: 1 addition & 0 deletions tests/components/reolink/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def get_mock_info(error=None, user_level="admin"):
host_mock.get_host_data = AsyncMock(return_value=None)
else:
host_mock.get_host_data = AsyncMock(side_effect=error)
host_mock.check_new_firmware = AsyncMock(return_value=False)
host_mock.unsubscribe = AsyncMock(return_value=True)
host_mock.logout = AsyncMock(return_value=True)
host_mock.mac_address = TEST_MAC
Expand Down