Add media_player platform to Lookin (#61337)
Co-authored-by: J. Nick Koston <nick@koston.org>pull/61458/head
parent
dc5888ab4a
commit
e5b04cedf3
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue