Add recording status for Philips TV (#94691)

pull/104181/head
Florian 2023-11-22 15:24:49 +01:00 committed by GitHub
parent 1f3f073df9
commit 01c49ba0e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 300 additions and 0 deletions

View File

@ -36,6 +36,7 @@ PLATFORMS = [
Platform.LIGHT,
Platform.REMOTE,
Platform.SWITCH,
Platform.BINARY_SENSOR,
]
LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,105 @@
"""Philips TV binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from haphilipsjs import PhilipsTV
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import PhilipsTVDataUpdateCoordinator
from .const import DOMAIN
from .entity import PhilipsJsEntity
@dataclass
class PhilipsTVBinarySensorEntityDescription(BinarySensorEntityDescription):
"""A entity description for Philips TV binary sensor."""
def __init__(self, recording_value, *args, **kwargs) -> None:
"""Set up a binary sensor entity description and add additional attributes."""
super().__init__(*args, **kwargs)
self.recording_value: str = recording_value
DESCRIPTIONS = (
PhilipsTVBinarySensorEntityDescription(
key="recording_ongoing",
translation_key="recording_ongoing",
icon="mdi:record-rec",
recording_value="RECORDING_ONGOING",
),
PhilipsTVBinarySensorEntityDescription(
key="recording_new",
translation_key="recording_new",
icon="mdi:new-box",
recording_value="RECORDING_NEW",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the configuration entry."""
coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
if (
coordinator.api.json_feature_supported("recordings", "List")
and coordinator.api.api_version == 6
):
async_add_entities(
PhilipsTVBinarySensorEntityRecordingType(coordinator, description)
for description in DESCRIPTIONS
)
def _check_for_recording_entry(api: PhilipsTV, entry: str, value: str) -> bool:
"""Return True if at least one specified value is available within entry of list."""
for rec in api.recordings_list["recordings"]:
if rec.get(entry) == value:
return True
return False
class PhilipsTVBinarySensorEntityRecordingType(PhilipsJsEntity, BinarySensorEntity):
"""A Philips TV binary sensor class, which allows multiple entities given by a BinarySensorEntityDescription."""
entity_description: PhilipsTVBinarySensorEntityDescription
def __init__(
self,
coordinator: PhilipsTVDataUpdateCoordinator,
description: PhilipsTVBinarySensorEntityDescription,
) -> None:
"""Initialize entity class."""
self.entity_description = description
self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"
self._attr_device_info = coordinator.device_info
self._attr_is_on = _check_for_recording_entry(
coordinator.api,
"RecordingType",
description.recording_value,
)
super().__init__(coordinator)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator and set is_on true if one specified value is available within given entry of list."""
self._attr_is_on = _check_for_recording_entry(
self.coordinator.api,
"RecordingType",
self.entity_description.recording_value,
)
super()._handle_coordinator_update()

View File

@ -44,6 +44,14 @@
}
},
"entity": {
"binary_sensor": {
"recording_new": {
"name": "New recording available"
},
"recording_ongoing": {
"name": "Recording ongoing"
}
},
"light": {
"ambilight": {
"name": "Ambilight"

View File

@ -73,3 +73,129 @@ MOCK_CONFIG_PAIRED = {
}
MOCK_ENTITY_ID = "media_player.philips_tv"
MOCK_RECORDINGS_LIST = {
"version": "253.91",
"recordings": [
{
"RecordingId": 36,
"RecordingType": "RECORDING_ONGOING",
"IsIpEpgRec": False,
"ccid": 2091,
"StartTime": 1676833531,
"Duration": 569,
"MarginStart": 0,
"MarginEnd": 0,
"EventId": 47369,
"EITVersion": 0,
"RetentionInfo": 0,
"EventInfo": "This is a event info which is not rejected by codespell.",
"EventExtendedInfo": "",
"EventGenre": "8",
"RecName": "Terra X",
"SeriesID": "None",
"SeasonNo": 0,
"EpisodeNo": 0,
"EpisodeCount": 72300,
"ProgramNumber": 11110,
"EventRating": 0,
"hasDot": True,
"isFTARecording": False,
"LastPinChangedTime": 0,
"Version": 344,
"HasCicamPin": False,
"HasLicenseFile": False,
"Size": 0,
"ResumeInfo": 0,
"IsPartial": False,
"AutoMarginStart": 0,
"AutoMarginEnd": 0,
"ServerRecordingId": -1,
"ActualStartTime": 1676833531,
"ProgramDuration": 0,
"IsRadio": False,
"EITSource": "EIT_SOURCE_PF",
"RecError": "REC_ERROR_NONE",
},
{
"RecordingId": 35,
"RecordingType": "RECORDING_NEW",
"IsIpEpgRec": False,
"ccid": 2091,
"StartTime": 1676832212,
"Duration": 22,
"MarginStart": 0,
"MarginEnd": 0,
"EventId": 47369,
"EITVersion": 0,
"RetentionInfo": -1,
"EventInfo": "This is another event info which is not rejected by codespell.",
"EventExtendedInfo": "",
"EventGenre": "8",
"RecName": "Terra X",
"SeriesID": "None",
"SeasonNo": 0,
"EpisodeNo": 0,
"EpisodeCount": 70980,
"ProgramNumber": 11110,
"EventRating": 0,
"hasDot": True,
"isFTARecording": False,
"LastPinChangedTime": 0,
"Version": 339,
"HasCicamPin": False,
"HasLicenseFile": False,
"Size": 0,
"ResumeInfo": 0,
"IsPartial": False,
"AutoMarginStart": 0,
"AutoMarginEnd": 0,
"ServerRecordingId": -1,
"ActualStartTime": 1676832212,
"ProgramDuration": 0,
"IsRadio": False,
"EITSource": "EIT_SOURCE_PF",
"RecError": "REC_ERROR_NONE",
},
{
"RecordingId": 34,
"RecordingType": "RECORDING_PARTIALLY_VIEWED",
"IsIpEpgRec": False,
"ccid": 2091,
"StartTime": 1676677580,
"Duration": 484,
"MarginStart": 0,
"MarginEnd": 0,
"EventId": -1,
"EITVersion": 0,
"RetentionInfo": -1,
"EventInfo": "\n\nAlpine Ski-WM: Parallel-Event, Übertragung aus Méribel/Frankreich\n\n14:10: Biathlon-WM (AD): 20 km Einzel Männer, Übertragung aus Oberhof\nHD-Produktion",
"EventExtendedInfo": "",
"EventGenre": "4",
"RecName": "ZDF HD 2023-02-18 00:46",
"SeriesID": "None",
"SeasonNo": 0,
"EpisodeNo": 0,
"EpisodeCount": 2760,
"ProgramNumber": 11110,
"EventRating": 0,
"hasDot": True,
"isFTARecording": False,
"LastPinChangedTime": 0,
"Version": 328,
"HasCicamPin": False,
"HasLicenseFile": False,
"Size": 0,
"ResumeInfo": 56,
"IsPartial": False,
"AutoMarginStart": 0,
"AutoMarginEnd": 0,
"ServerRecordingId": -1,
"ActualStartTime": 1676677581,
"ProgramDuration": 0,
"IsRadio": False,
"EITSource": "EIT_SOURCE_PF",
"RecError": "REC_ERROR_NONE",
},
],
}

View File

@ -0,0 +1,60 @@
"""The tests for philips_js binary_sensor."""
import pytest
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from . import MOCK_NAME, MOCK_RECORDINGS_LIST
ID_RECORDING_AVAILABLE = (
"binary_sensor." + MOCK_NAME.replace(" ", "_").lower() + "_new_recording_available"
)
ID_RECORDING_ONGOING = (
"binary_sensor." + MOCK_NAME.replace(" ", "_").lower() + "_recording_ongoing"
)
@pytest.fixture
async def mock_tv_api_invalid(mock_tv):
"""Set up a invalid mock_tv with should not create sensors."""
mock_tv.secured_transport = True
mock_tv.api_version = 1
mock_tv.recordings_list = None
return mock_tv
@pytest.fixture
async def mock_tv_api_valid(mock_tv):
"""Set up a valid mock_tv with should create sensors."""
mock_tv.secured_transport = True
mock_tv.api_version = 6
mock_tv.recordings_list = MOCK_RECORDINGS_LIST
return mock_tv
async def test_recordings_list_invalid(
mock_tv_api_invalid, mock_config_entry, hass: HomeAssistant
) -> None:
"""Test if sensors are not created if mock_tv is invalid."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
state = hass.states.get(ID_RECORDING_AVAILABLE)
assert state is None
state = hass.states.get(ID_RECORDING_ONGOING)
assert state is None
async def test_recordings_list_valid(
mock_tv_api_valid, mock_config_entry, hass: HomeAssistant
) -> None:
"""Test if sensors are created correctly."""
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
state = hass.states.get(ID_RECORDING_AVAILABLE)
assert state.state is STATE_ON
state = hass.states.get(ID_RECORDING_ONGOING)
assert state.state is STATE_ON