|
| 1 | +import logging |
| 2 | +import io |
| 3 | +import itertools |
| 4 | +import aiohttp |
| 5 | +import colorgram |
| 6 | + |
| 7 | +from homeassistant.core import HomeAssistant |
| 8 | +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType |
| 9 | +from homeassistant.helpers.entity_platform import AddEntitiesCallback |
| 10 | +from homeassistant.components.media_player import MediaPlayerEntity, MediaPlayerEntityFeature, \ |
| 11 | + MediaType, MediaPlayerDeviceClass |
| 12 | +from homeassistant.components import media_source |
| 13 | +from homeassistant.components.media_player.browse_media import ( |
| 14 | + BrowseMedia, |
| 15 | + async_process_play_media_url, |
| 16 | +) |
| 17 | +from homeassistant.const import ATTR_ENTITY_ID |
| 18 | +from homeassistant.components import light |
| 19 | + |
| 20 | +from . import const |
| 21 | +from .entity_resolver import expand_entities |
| 22 | + |
| 23 | +_LOGGER = logging.getLogger(__name__) |
| 24 | + |
| 25 | + |
| 26 | +async def async_setup_platform( |
| 27 | + hass: HomeAssistant, |
| 28 | + config: ConfigType, |
| 29 | + add_entities: AddEntitiesCallback, |
| 30 | + discovery_info: DiscoveryInfoType | None = None, |
| 31 | +) -> None: |
| 32 | + _LOGGER.info('LightCast media_player setup: %s', config) |
| 33 | + |
| 34 | + cast_device = LightCastPlayer( |
| 35 | + hass, |
| 36 | + config[const.CONF_NAME], |
| 37 | + config[const.CONF_TARGET], |
| 38 | + ) |
| 39 | + |
| 40 | + add_entities([cast_device]) |
| 41 | + |
| 42 | + |
| 43 | +class LightCastPlayer(MediaPlayerEntity): |
| 44 | + _attr_available = True |
| 45 | + _attr_supported_features = MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.BROWSE_MEDIA |
| 46 | + _attr_device_class = MediaPlayerDeviceClass.TV |
| 47 | + |
| 48 | + def __init__(self, hass: HomeAssistant, name: str, device_target: str) -> None: |
| 49 | + self.hass = hass |
| 50 | + self.device_target = device_target |
| 51 | + self._attr_name = name |
| 52 | + |
| 53 | + async def async_browse_media(self, media_content_type: str | None = None, |
| 54 | + media_content_id: str | None = None) -> BrowseMedia: |
| 55 | + """ |
| 56 | + Implement MediaPlayerEntityFeature.BROWSE_MEDIA |
| 57 | + :param media_content_type: |
| 58 | + :param media_content_id: |
| 59 | + :return: |
| 60 | + """ |
| 61 | + return await media_source.async_browse_media( |
| 62 | + self.hass, |
| 63 | + media_content_id, |
| 64 | + content_filter=lambda item: item.media_content_type.startswith('image/') |
| 65 | + ) |
| 66 | + |
| 67 | + async def process_image(self, media_type: str, media_id: str) -> None: |
| 68 | + """ |
| 69 | + Download an image found at media_id to an in-memory buffer. |
| 70 | + Resolve the entity list of lights referenced by device_target which are currently in the on state |
| 71 | + and extract that many colors from the image using colorgram. |
| 72 | + For each pair of light and color, call light.turn_on with that color |
| 73 | + :param media_type: |
| 74 | + :param media_id: |
| 75 | + """ |
| 76 | + _LOGGER.info('Processing image %s: %s', media_type, media_id) |
| 77 | + |
| 78 | + found_entities = expand_entities(self.hass, self.device_target) |
| 79 | + _LOGGER.info('Found %d entities matching %s', len(found_entities), self.device_target) |
| 80 | + |
| 81 | + if not found_entities: |
| 82 | + _LOGGER.warning('No entities were matched') |
| 83 | + return |
| 84 | + |
| 85 | + valid_entities = [e for e in found_entities if e.state == 'on'] |
| 86 | + if not valid_entities: |
| 87 | + _LOGGER.warning('No targets are on') |
| 88 | + return |
| 89 | + |
| 90 | + async with aiohttp.ClientSession() as session: |
| 91 | + async with session.get(media_id) as response: |
| 92 | + response_data = await response.read() |
| 93 | + |
| 94 | + n_colors = len(valid_entities) |
| 95 | + _LOGGER.info('Downloaded image, extracting palette of %d colors', n_colors) |
| 96 | + |
| 97 | + extracted_colors = colorgram.extract(io.BytesIO(response_data), n_colors) |
| 98 | + _LOGGER.info('Extracted palette of %d colors from image', len(extracted_colors)) |
| 99 | + |
| 100 | + for e, color in zip(valid_entities, itertools.cycle(extracted_colors)): |
| 101 | + rgb_color = [color.rgb.r, color.rgb.g, color.rgb.b] |
| 102 | + _LOGGER.info('Set light %s to %s', e.entity_id, rgb_color) |
| 103 | + await self.hass.services.async_call( |
| 104 | + light.DOMAIN, |
| 105 | + light.SERVICE_TURN_ON, |
| 106 | + { |
| 107 | + ATTR_ENTITY_ID: e.entity_id, |
| 108 | + light.ATTR_RGB_COLOR: rgb_color |
| 109 | + } |
| 110 | + ) |
| 111 | + |
| 112 | + async def async_play_media(self, media_type: MediaType, media_id: str, **kwargs: any) -> None: |
| 113 | + if media_source.is_media_source_id(media_id): |
| 114 | + play_item = await media_source.async_resolve_media(self.hass, media_id, self.entity_id) |
| 115 | + media_id = async_process_play_media_url(self.hass, play_item.url) |
| 116 | + |
| 117 | + self.hass.async_create_task( |
| 118 | + self.process_image(media_type, media_id) |
| 119 | + ) |
0 commit comments