mirror of
https://github.com/home-assistant/core.git
synced 2026-03-02 07:29:28 +00:00
Add volume control to Foscam Upgrade dependencies (#150618)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
committed by
GitHub
parent
2fc2bb97fc
commit
e65b4292b2
@@ -16,7 +16,7 @@ from .config_flow import DEFAULT_RTSP_PORT
|
||||
from .const import CONF_RTSP_PORT, LOGGER
|
||||
from .coordinator import FoscamConfigEntry, FoscamCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CAMERA, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.CAMERA, Platform.NUMBER, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bool:
|
||||
@@ -29,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FoscamConfigEntry) -> bo
|
||||
entry.data[CONF_PASSWORD],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
coordinator = FoscamCoordinator(hass, entry, session)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
|
||||
@@ -30,10 +30,11 @@ class FoscamDeviceInfo:
|
||||
is_open_white_light: bool
|
||||
is_siren_alarm: bool
|
||||
|
||||
volume: int
|
||||
device_volume: int
|
||||
speak_volume: int
|
||||
is_turn_off_volume: bool
|
||||
is_turn_off_light: bool
|
||||
supports_speak_volume_adjustment: bool
|
||||
|
||||
is_open_wdr: bool | None = None
|
||||
is_open_hdr: bool | None = None
|
||||
@@ -118,6 +119,14 @@ class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]):
|
||||
mode = is_open_hdr_data["mode"] if ret_hdr == 0 and is_open_hdr_data else 0
|
||||
is_open_hdr = bool(int(mode))
|
||||
|
||||
ret_sw, software_capabilities = self.session.getSWCapabilities()
|
||||
|
||||
supports_speak_volume_adjustment_val = (
|
||||
bool(int(software_capabilities.get("swCapabilities1")) & 32)
|
||||
if ret_sw == 0
|
||||
else False
|
||||
)
|
||||
|
||||
return FoscamDeviceInfo(
|
||||
dev_info=dev_info,
|
||||
product_info=product_info,
|
||||
@@ -127,10 +136,11 @@ class FoscamCoordinator(DataUpdateCoordinator[FoscamDeviceInfo]):
|
||||
is_asleep=is_asleep,
|
||||
is_open_white_light=is_open_white_light_val,
|
||||
is_siren_alarm=is_siren_alarm_val,
|
||||
volume=volume_val,
|
||||
device_volume=volume_val,
|
||||
speak_volume=speak_volume_val,
|
||||
is_turn_off_volume=is_turn_off_volume_val,
|
||||
is_turn_off_light=is_turn_off_light_val,
|
||||
supports_speak_volume_adjustment=supports_speak_volume_adjustment_val,
|
||||
is_open_wdr=is_open_wdr,
|
||||
is_open_hdr=is_open_hdr,
|
||||
)
|
||||
|
||||
@@ -13,6 +13,8 @@ from .coordinator import FoscamCoordinator
|
||||
class FoscamEntity(CoordinatorEntity[FoscamCoordinator]):
|
||||
"""Base entity for Foscam camera."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: FoscamCoordinator, config_entry_id: str) -> None:
|
||||
"""Initialize the base Foscam entity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
"wdr_switch": {
|
||||
"default": "mdi:alpha-w-box"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"device_volume": {
|
||||
"default": "mdi:volume-source"
|
||||
},
|
||||
"speak_volume": {
|
||||
"default": "mdi:account-voice"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
homeassistant/components/foscam/number.py
Normal file
93
homeassistant/components/foscam/number.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Foscam number platform for Home Assistant."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from libpyfoscamcgi import FoscamCamera
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import FoscamConfigEntry, FoscamCoordinator
|
||||
from .entity import FoscamEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FoscamNumberEntityDescription(NumberEntityDescription):
|
||||
"""A custom entity description with adjustable features."""
|
||||
|
||||
native_value_fn: Callable[[FoscamCoordinator], int]
|
||||
set_value_fn: Callable[[FoscamCamera, float], Any]
|
||||
exists_fn: Callable[[FoscamCoordinator], bool]
|
||||
|
||||
|
||||
NUMBER_DESCRIPTIONS: list[FoscamNumberEntityDescription] = [
|
||||
FoscamNumberEntityDescription(
|
||||
key="device_volume",
|
||||
translation_key="device_volume",
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_step=1,
|
||||
native_value_fn=lambda coordinator: coordinator.data.device_volume,
|
||||
set_value_fn=lambda session, value: session.setAudioVolume(value),
|
||||
exists_fn=lambda _: True,
|
||||
),
|
||||
FoscamNumberEntityDescription(
|
||||
key="speak_volume",
|
||||
translation_key="speak_volume",
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_step=1,
|
||||
native_value_fn=lambda coordinator: coordinator.data.speak_volume,
|
||||
set_value_fn=lambda session, value: session.setSpeakVolume(value),
|
||||
exists_fn=lambda coordinator: coordinator.data.supports_speak_volume_adjustment,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: FoscamConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up foscam number from a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
FoscamVolumeNumberEntity(coordinator, description)
|
||||
for description in NUMBER_DESCRIPTIONS
|
||||
if description.exists_fn is None or description.exists_fn(coordinator)
|
||||
)
|
||||
|
||||
|
||||
class FoscamVolumeNumberEntity(FoscamEntity, NumberEntity):
|
||||
"""Representation of a Foscam Smart AI number entity."""
|
||||
|
||||
entity_description: FoscamNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FoscamCoordinator,
|
||||
description: FoscamNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the data."""
|
||||
entry_id = coordinator.config_entry.entry_id
|
||||
super().__init__(coordinator, entry_id)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{entry_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.native_value_fn(self.coordinator)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the value."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.set_value_fn, self.coordinator.session, value
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -62,6 +62,14 @@
|
||||
"wdr_switch": {
|
||||
"name": "WDR"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"device_volume": {
|
||||
"name": "Device volume"
|
||||
},
|
||||
"speak_volume": {
|
||||
"name": "Speak volume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -121,7 +121,6 @@ async def async_setup_entry(
|
||||
"""Set up foscam switch from a config entry."""
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entities = []
|
||||
|
||||
@@ -146,7 +145,6 @@ async def async_setup_entry(
|
||||
class FoscamGenericSwitch(FoscamEntity, SwitchEntity):
|
||||
"""A generic switch class for Foscam entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: FoscamSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -75,7 +75,15 @@ def setup_mock_foscam_camera(mock_foscam_camera):
|
||||
mock_foscam_camera.getWdrMode.return_value = (0, {"mode": "0"})
|
||||
mock_foscam_camera.getHdrMode.return_value = (0, {"mode": "0"})
|
||||
mock_foscam_camera.get_motion_detect_config.return_value = (0, 1)
|
||||
|
||||
mock_foscam_camera.getSWCapabilities.return_value = (
|
||||
0,
|
||||
{
|
||||
"swCapabilities1": "100",
|
||||
"swCapbilities2": "100",
|
||||
"swCapbilities3": "100",
|
||||
"swCapbilities4": "100",
|
||||
},
|
||||
)
|
||||
return mock_foscam_camera
|
||||
|
||||
mock_foscam_camera.side_effect = configure_mock_on_init
|
||||
|
||||
115
tests/components/foscam/snapshots/test_number.ambr
Normal file
115
tests/components/foscam/snapshots/test_number.ambr
Normal file
@@ -0,0 +1,115 @@
|
||||
# serializer version: 1
|
||||
# name: test_number_entities[number.mock_title_device_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.mock_title_device_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Device volume',
|
||||
'platform': 'foscam',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'device_volume',
|
||||
'unique_id': '123ABC_device_volume',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.mock_title_device_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Device volume',
|
||||
'max': 100,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_title_device_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.mock_title_speak_volume-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': None,
|
||||
'entity_id': 'number.mock_title_speak_volume',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speak volume',
|
||||
'platform': 'foscam',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'speak_volume',
|
||||
'unique_id': '123ABC_speak_volume',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_number_entities[number.mock_title_speak_volume-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Mock Title Speak volume',
|
||||
'max': 100,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.mock_title_speak_volume',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '100',
|
||||
})
|
||||
# ---
|
||||
62
tests/components/foscam/test_number.py
Normal file
62
tests/components/foscam/test_number.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Test the Foscam number platform."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.foscam.const import DOMAIN
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import setup_mock_foscam_camera
|
||||
from .const import ENTRY_ID, VALID_CONFIG
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_number_entities(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test creation of number entities."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
# Mock a valid camera instance
|
||||
patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera,
|
||||
patch("homeassistant.components.foscam.PLATFORMS", [Platform.NUMBER]),
|
||||
):
|
||||
setup_mock_foscam_camera(mock_foscam_camera)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
|
||||
|
||||
|
||||
async def test_setting_number(hass: HomeAssistant) -> None:
|
||||
"""Test setting a number entity calls the correct method on the camera."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, entry_id=ENTRY_ID)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.foscam.FoscamCamera") as mock_foscam_camera:
|
||||
setup_mock_foscam_camera(mock_foscam_camera)
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: "number.mock_title_device_volume",
|
||||
ATTR_VALUE: 42,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
mock_foscam_camera.setAudioVolume.assert_called_once_with(42)
|
||||
Reference in New Issue
Block a user