Add media_player platform to Lookin (#61337)

Co-authored-by: J. Nick Koston <nick@koston.org>
pull/61458/head
Anton Malko 2021-12-10 21:52:51 +03:00 committed by GitHub
parent dc5888ab4a
commit e5b04cedf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 264 additions and 69 deletions

View File

@ -604,6 +604,7 @@ omit =
homeassistant/components/lookin/models.py
homeassistant/components/lookin/sensor.py
homeassistant/components/lookin/climate.py
homeassistant/components/lookin/media_player.py
homeassistant/components/luci/device_tracker.py
homeassistant/components/luftdaten/__init__.py
homeassistant/components/luftdaten/sensor.py

View File

@ -10,9 +10,9 @@ from aiolookin import (
LookInHttpProtocol,
LookinUDPSubscriptions,
MeteoSensor,
SensorID,
start_lookin_udp,
)
from aiolookin.models import UDPCommandType, UDPEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
@ -53,22 +53,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await meteo_coordinator.async_config_entry_first_refresh()
@callback
def _async_meteo_push_update(msg: dict[str, str]) -> None:
def _async_meteo_push_update(event: UDPEvent) -> None:
"""Process an update pushed via UDP."""
if int(msg["event_id"]):
return
LOGGER.debug("Processing push message for meteo sensor: %s", msg)
LOGGER.debug("Processing push message for meteo sensor: %s", event)
meteo: MeteoSensor = meteo_coordinator.data
meteo.update_from_value(msg["value"])
meteo.update_from_value(event.value)
meteo_coordinator.async_set_updated_data(meteo)
lookin_udp_subs = LookinUDPSubscriptions()
entry.async_on_unload(
lookin_udp_subs.subscribe_sensor(
lookin_device.id, SensorID.Meteo, None, _async_meteo_push_update
lookin_udp_subs.subscribe_event(
lookin_device.id, UDPCommandType.meteo, None, _async_meteo_push_update
)
)
entry.async_on_unload(await start_lookin_udp(lookin_udp_subs))
entry.async_on_unload(await start_lookin_udp(lookin_udp_subs, lookin_device.id))
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LookinData(
lookin_udp_subs=lookin_udp_subs,

View File

@ -6,7 +6,8 @@ from datetime import timedelta
import logging
from typing import Any, Final, cast
from aiolookin import Climate, MeteoSensor, SensorID
from aiolookin import Climate, MeteoSensor
from aiolookin.models import UDPCommandType, UDPEvent
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
@ -116,6 +117,7 @@ async def async_setup_entry(
class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
"""An aircon or heat pump."""
_attr_current_humidity: float | None = None # type: ignore
_attr_temperature_unit = TEMP_CELSIUS
_attr_supported_features: int = SUPPORT_FLAGS
_attr_fan_modes: list[str] = LOOKIN_FAN_MODE_IDX_TO_HASS
@ -198,6 +200,7 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
def _async_update_from_data(self) -> None:
"""Update attrs from data."""
meteo_data: MeteoSensor = self._meteo_coordinator.data
self._attr_current_temperature = meteo_data.temperature
self._attr_current_humidity = int(meteo_data.humidity)
self._attr_target_temperature = self._climate.temp_celsius
@ -205,6 +208,12 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
self._attr_swing_mode = LOOKIN_SWING_MODE_IDX_TO_HASS[self._climate.swing_mode]
self._attr_hvac_mode = LOOKIN_HVAC_MODE_IDX_TO_HASS[self._climate.hvac_mode]
@callback
def _async_update_meteo_from_value(self, event: UDPEvent) -> None:
"""Update temperature and humidity from UDP event."""
self._attr_current_temperature = float(int(event.value[:4], 16)) / 10
self._attr_current_humidity = float(int(event.value[-4:], 16)) / 10
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
@ -212,20 +221,28 @@ class ConditionerEntity(LookinCoordinatorEntity, ClimateEntity):
super()._handle_coordinator_update()
@callback
def _async_push_update(self, msg: dict[str, str]) -> None:
def _async_push_update(self, event: UDPEvent) -> None:
"""Process an update pushed via UDP."""
LOGGER.debug("Processing push message for %s: %s", self.entity_id, msg)
self._climate.update_from_status(msg["value"])
LOGGER.debug("Processing push message for %s: %s", self.entity_id, event)
self._climate.update_from_status(event.value)
self.coordinator.async_set_updated_data(self._climate)
async def async_added_to_hass(self) -> None:
"""Call when the entity is added to hass."""
self.async_on_remove(
self._lookin_udp_subs.subscribe_sensor(
self._lookin_device.id, SensorID.IR, self._uuid, self._async_push_update
self._lookin_udp_subs.subscribe_event(
self._lookin_device.id,
UDPCommandType.ir,
self._uuid,
self._async_push_update,
)
)
self.async_on_remove(
self._meteo_coordinator.async_add_listener(self._handle_coordinator_update)
self._lookin_udp_subs.subscribe_event(
self._lookin_device.id,
UDPCommandType.meteo,
None,
self._async_update_meteo_from_value,
)
)
return await super().async_added_to_hass()

View File

@ -5,5 +5,7 @@ from typing import Final
from homeassistant.const import Platform
MODEL_NAMES: Final = ["LOOKin Remote", "LOOKin Remote", "LOOKin Remote2"]
DOMAIN: Final = "lookin"
PLATFORMS: Final = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS: Final = [Platform.CLIMATE, Platform.MEDIA_PLAYER, Platform.SENSOR]

View File

@ -4,13 +4,13 @@ from __future__ import annotations
from aiolookin import POWER_CMD, POWER_OFF_CMD, POWER_ON_CMD, Climate, Remote
from aiolookin.models import Device
from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import DOMAIN
from .const import DOMAIN, MODEL_NAMES
from .models import LookinData
@ -20,7 +20,7 @@ def _lookin_device_to_device_info(lookin_device: Device) -> DeviceInfo:
identifiers={(DOMAIN, lookin_device.id)},
name=lookin_device.name,
manufacturer="LOOKin",
model="LOOKin Remote2",
model=MODEL_NAMES[lookin_device.model],
sw_version=lookin_device.firmware,
)
@ -46,19 +46,6 @@ class LookinDeviceMixIn:
self._lookin_udp_subs = lookin_data.lookin_udp_subs
class LookinDeviceEntity(LookinDeviceMixIn, Entity):
"""A lookin device entity on the device itself."""
_attr_should_poll = False
def __init__(self, lookin_data: LookinData) -> None:
"""Init the lookin device entity."""
self._set_lookin_device_attrs(lookin_data)
self._attr_device_info = _lookin_device_to_device_info(
lookin_data.lookin_device
)
class LookinDeviceCoordinatorEntity(LookinDeviceMixIn, CoordinatorEntity):
"""A lookin device entity on the device itself that uses the coordinator."""
@ -89,34 +76,6 @@ class LookinEntityMixIn:
self._function_names = {function.name for function in self._device.functions}
class LookinEntity(LookinDeviceMixIn, LookinEntityMixIn, Entity):
"""A base class for lookin entities."""
_attr_should_poll = False
_attr_assumed_state = True
def __init__(
self,
uuid: str,
device: Remote | Climate,
lookin_data: LookinData,
) -> None:
"""Init the base entity."""
self._set_lookin_device_attrs(lookin_data)
self._set_lookin_entity_attrs(uuid, device, lookin_data)
self._attr_device_info = _lookin_controlled_device_to_device_info(
self._lookin_device, uuid, device
)
self._attr_unique_id = uuid
self._attr_name = device.name
async def _async_send_command(self, command: str) -> None:
"""Send command from saved IR device."""
await self._lookin_protocol.send_command(
uuid=self._uuid, command=command, signal="FF"
)
class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorEntity):
"""A lookin device entity for an external device that uses the coordinator."""
@ -147,17 +106,18 @@ class LookinCoordinatorEntity(LookinDeviceMixIn, LookinEntityMixIn, CoordinatorE
)
class LookinPowerEntity(LookinEntity):
class LookinPowerEntity(LookinCoordinatorEntity):
"""A Lookin entity that has a power on and power off command."""
def __init__(
self,
coordinator: DataUpdateCoordinator,
uuid: str,
device: Remote | Climate,
lookin_data: LookinData,
) -> None:
"""Init the power entity."""
super().__init__(uuid, device, lookin_data)
super().__init__(coordinator, uuid, device, lookin_data)
self._power_on_command: str = POWER_CMD
self._power_off_command: str = POWER_CMD
if POWER_ON_CMD in self._function_names:

View File

@ -3,7 +3,7 @@
"name": "LOOKin",
"documentation": "https://www.home-assistant.io/integrations/lookin/",
"codeowners": ["@ANMalko"],
"requirements": ["aiolookin==0.0.4"],
"requirements": ["aiolookin==0.1.0"],
"zeroconf": ["_lookin._tcp.local."],
"config_flow": true,
"iot_class": "local_push"

View File

@ -0,0 +1,213 @@
"""The lookin integration light platform."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from datetime import timedelta
import logging
from typing import Any, cast
from aiolookin import Remote
from aiolookin.models import UDPCommandType, UDPEvent
from homeassistant.components.media_player import (
DEVICE_CLASS_RECEIVER,
DEVICE_CLASS_TV,
MediaPlayerEntity,
)
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_ON, STATE_STANDBY
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
from .entity import LookinPowerEntity
from .models import LookinData
LOGGER = logging.getLogger(__name__)
_TYPE_TO_DEVICE_CLASS = {"01": DEVICE_CLASS_TV, "02": DEVICE_CLASS_RECEIVER}
_FUNCTION_NAME_TO_FEATURE = {
"power": SUPPORT_TURN_OFF,
"poweron": SUPPORT_TURN_ON,
"poweroff": SUPPORT_TURN_OFF,
"mute": SUPPORT_VOLUME_MUTE,
"volup": SUPPORT_VOLUME_STEP,
"chup": SUPPORT_NEXT_TRACK,
"chdown": SUPPORT_PREVIOUS_TRACK,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the media_player platform for lookin from a config entry."""
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
entities = []
for remote in lookin_data.devices:
if remote["Type"] not in _TYPE_TO_DEVICE_CLASS:
continue
uuid = remote["UUID"]
def _wrap_async_update(
uuid: str,
) -> Callable[[], Coroutine[None, Any, Remote]]:
"""Create a function to capture the uuid cell variable."""
async def _async_update() -> Remote:
return await lookin_data.lookin_protocol.get_remote(uuid)
return _async_update
coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{config_entry.title} {uuid}",
update_method=_wrap_async_update(uuid),
update_interval=timedelta(
seconds=60
), # Updates are pushed (fallback is polling)
)
await coordinator.async_refresh()
device: Remote = coordinator.data
entities.append(
LookinMedia(
uuid=uuid,
device=device,
lookin_data=lookin_data,
device_class=_TYPE_TO_DEVICE_CLASS[remote["Type"]],
coordinator=coordinator,
)
)
async_add_entities(entities)
class LookinMedia(LookinPowerEntity, MediaPlayerEntity):
"""A lookin media player."""
_attr_should_poll = False
def __init__(
self,
uuid: str,
device: Remote,
lookin_data: LookinData,
device_class: str,
coordinator: DataUpdateCoordinator,
) -> None:
"""Init the lookin media player."""
self._attr_device_class = device_class
self._attr_supported_features: int = 0
self._attr_state = None
self._attr_is_volume_muted: bool = False
super().__init__(coordinator, uuid, device, lookin_data)
for function_name, feature in _FUNCTION_NAME_TO_FEATURE.items():
if function_name in self._function_names:
self._attr_supported_features |= feature
self._attr_name = self._remote.name
self._async_update_from_data()
@property
def _remote(self) -> Remote:
return cast(Remote, self.coordinator.data)
async def async_volume_up(self) -> None:
"""Turn volume up for media player."""
await self._async_send_command("volup")
async def async_volume_down(self) -> None:
"""Turn volume down for media player."""
await self._async_send_command("voldown")
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self._async_send_command("chdown")
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self._async_send_command("chup")
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
await self._async_send_command("mute")
self._attr_is_volume_muted = not self.is_volume_muted
self.async_write_ha_state()
async def async_turn_off(self) -> None:
"""Turn the media player off."""
await self._async_send_command(self._power_off_command)
self._attr_state = STATE_STANDBY
self.async_write_ha_state()
async def async_turn_on(self) -> None:
"""Turn the media player on."""
await self._async_send_command(self._power_on_command)
self._attr_state = STATE_ON
self.async_write_ha_state()
def _update_from_status(self, status: str) -> None:
"""Update media property from status.
00F0
0 - 0/1 on/off
0 - sourse
F - volume, 0 - muted, 1 - volume up, F - volume down
0 - not used
"""
if len(status) != 4:
return
state = status[0]
mute = status[2]
self._attr_state = STATE_STANDBY if state == "1" else STATE_ON
self._attr_is_volume_muted = mute == "0"
def _async_push_update(self, event: UDPEvent) -> None:
"""Process an update pushed via UDP."""
LOGGER.debug("Processing push message for %s: %s", self.entity_id, event)
self._update_from_status(event.value)
self.coordinator.async_set_updated_data(self._remote)
self.async_write_ha_state()
async def _async_push_update_device(self, event: UDPEvent) -> None:
"""Process an update pushed via UDP."""
LOGGER.debug("Processing push message for %s: %s", self.entity_id, event)
await self.coordinator.async_refresh()
self._attr_name = self._remote.name
async def async_added_to_hass(self) -> None:
"""Call when the entity is added to hass."""
self.async_on_remove(
self._lookin_udp_subs.subscribe_event(
self._lookin_device.id,
UDPCommandType.ir,
self._uuid,
self._async_push_update,
)
)
self.async_on_remove(
self._lookin_udp_subs.subscribe_event(
self._lookin_device.id,
UDPCommandType.data,
self._uuid,
self._async_push_update_device,
)
)
def _async_update_from_data(self) -> None:
"""Update attrs from data."""
self._update_from_status(self._remote.status)

View File

@ -48,9 +48,13 @@ async def async_setup_entry(
"""Set up lookin sensors from the config entry."""
lookin_data: LookinData = hass.data[DOMAIN][config_entry.entry_id]
async_add_entities(
[LookinSensorEntity(description, lookin_data) for description in SENSOR_TYPES]
)
if lookin_data.lookin_device.model >= 2:
async_add_entities(
[
LookinSensorEntity(description, lookin_data)
for description in SENSOR_TYPES
]
)
class LookinSensorEntity(LookinDeviceCoordinatorEntity, SensorEntity):

View File

@ -213,7 +213,7 @@ aiolifx_effects==0.2.2
aiolip==1.1.6
# homeassistant.components.lookin
aiolookin==0.0.4
aiolookin==0.1.0
# homeassistant.components.lyric
aiolyric==1.0.8

View File

@ -146,7 +146,7 @@ aiokafka==0.6.0
aiolip==1.1.6
# homeassistant.components.lookin
aiolookin==0.0.4
aiolookin==0.1.0
# homeassistant.components.lyric
aiolyric==1.0.8