class Chromecast(MediaPlayer): """The Chromecast item""" config_schema = vol.Schema({ vol.Required("host"): vol.Coerce(str), vol.Required("port", default=8009): vol.Coerce(int) }, extra=vol.ALLOW_EXTRA) _chromecast: Optional[pychromecast.Chromecast] = None media_status = None device: Optional[DeviceStatus] position = StateDef(poll_interval=1, log_state=False) async def init(self) -> Optional[bool]: """Initialises the Chromecast item""" self.storage = Storage( f"item_data/{self.unique_identifier}", 1, core=self.core, storage_init=lambda: None, loader=lambda data: data and DeviceStatus( **{**data, "uuid": UUID(data["uuid"])}), dumper=lambda data: {**data._asdict(), "uuid": data.uuid.hex}) self.device = pychromecast.get_device_status( self.cfg["host"]) or self.storage.load_data() if not self.device: LOGGER.error( "Could not connect to chromecast at %s:%s", self.cfg["host"], self.cfg["port"]) return self.storage.schedule_save(self.device) def _connect(): chromecast = pychromecast.Chromecast( host=self.cfg["host"], port=self.cfg["port"], device=self.device) chromecast.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) chromecast.start() self._chromecast = chromecast self.core.loop.run_in_executor(None, _connect) return False @position.getter() async def get_position(self) -> Optional[float]: """Position getter""" if not self.media_status: return None position = self.media_status.current_time if self.media_status.player_state == "PLAYING": position += (datetime.utcnow() - self.media_status.last_updated).seconds return position @position.setter() async def set_position(self, position: int) -> Dict[str, Any]: value = max( 0, min(position, cast(int, await self.states.get("duration")))) self._chromecast.media_controller.seek(value) return {"position": value} @action("pause") async def action_pause(self) -> None: self._chromecast.media_controller.pause() async def set_playing(self, playing: bool) -> Dict[str, Any]: if playing: self._chromecast.media_controller.play() else: self._chromecast.media_controller.pause() return {"playing": playing} async def set_volume(self, volume: int) -> Dict[str, Any]: self._chromecast.set_volume(volume / 100) return {"volume": volume} @action("play") async def action_play(self) -> None: """Action: Play""" self._chromecast.media_controller.play() @action("play_url") async def action_play_url(self, url: str, mime: str) -> None: """Action: Play URL""" self._chromecast.media_controller.play_media( url=url, content_type=mime) @action("previous") async def action_previous(self) -> None: """Action: Previous""" self._chromecast.media_controller.rewind() @action("next") async def action_next(self) -> None: """Action: Next""" self._chromecast.media_controller.skip() @action("stop") async def action_stop(self) -> None: """Action: stop""" if self._chromecast.media_controller.is_active: self._chromecast.media_controller.stop() @action("quit") async def action_quit(self) -> None: """Action: Quit casting""" self._chromecast.quit_app() def new_media_status(self, status) -> None: """Handle new media status""" self.core.loop.run_in_executor(None, self.update_media_status, status) def new_connection_status(self, status) -> None: """Handles connection status updates""" if status.status == 'CONNECTED': self.update_status(ItemStatus.ONLINE) else: self.update_status(ItemStatus.OFFLINE) def update_media_status(self, status) -> None: """Update media status""" self.media_status = status self.states.bulk_update( position=status.current_time, playing=status.player_state == "PLAYING", content_type=status.content_type, volume=int(status.volume_level * 100), duration=status.duration, title=status.title, artist=status.artist, album=status.album_name, cover=status.images[0].url if status.images else None ) async def stop(self) -> None: if not self._chromecast: return with suppress(Exception): self._chromecast.disconnect(1)
async def constructor( cls, identifier: str, name: str, cfg: Dict[str, Any], state_defaults: Dict[str, Any], core: "Core", unique_identifier: str) -> "Item": cfg = cast(Dict[str, Any], cls.config_schema(cfg or {})) item = cls() item.entities = {} item.core = core item.identifier = identifier item.unique_identifier = unique_identifier item.name = name item.cfg = cfg item.actions = {} item.states = StateProxy(item, core) item.status = ItemStatus.OFFLINE storage = Storage( f"item_data/{unique_identifier}", 1, core=core, storage_init=lambda: {}, loader=item.load_config, dumper=item.dump_config) storage_data: StorageConfig = storage.load_data() api = APIClient( core.loop, cfg["host"], cfg["port"], cfg["password"], client_info=f"HomeControl {VERSION_STRING}" ) item.api = api connected, _ = await asyncio.wait( {core.loop.create_task(item.connect())}, timeout=6) if connected: entities, services = await api.list_entities_services() for service in services: item.actions[service.name] = partial( item._run_service, service) device_info = await api.device_info() storage_data.entities.clear() for entity in entities: storage_data.entities.append(StorageEntity( entity=entity, entity_type=type(entity).__name__ )) storage.schedule_save(storage_data) else: entities = [storage_entity.entity for storage_entity in storage_data.entities] device_info = storage_data.device_info version_state = StateDef(default=device_info.esphome_version) version_state.register_state(item.states, "version", item) for entity in entities: entity_type = ENTITY_TYPES.get(type(entity).__name__) if not entity_type: LOGGER.info("Did not add entity %s", entity) continue unique_e_identifier = f"{unique_identifier}_{entity.object_id}" entity_identifier = f"{identifier}_{entity.object_id}" entity_item = await entity_type.constructor( identifier=entity_identifier, name=entity.name, core=core, unique_identifier=unique_e_identifier, device=item, entity=entity ) item.entities[entity.key] = entity_item await core.item_manager.register_item(entity_item) return item
class ItemManager: """ ItemManager manages all your stateful items """ core: "Core" items: Dict[str, Item] yaml_cfg: List[dict] item_config: Dict[str, StorageEntry] def __init__(self, core: "Core"): self.core = core self.items = {} self.item_constructors = {} self.storage = Storage("items", 1, core=self.core, storage_init=lambda: {}, loader=self._load_items, dumper=self._dump_items) self.item_config = self.storage.load_data() async def init(self) -> None: """Initialise the items from configuration""" self.yaml_cfg = cast( List[dict], await self.core.cfg.register_domain("items", schema=CONFIG_SCHEMA, default=[])) self.load_yaml_config() self.core.loop.create_task(self.init_from_storage()) async def init_from_storage(self) -> None: """Initializes the items configured in the storage""" await asyncio.gather(*(self.create_from_storage_entry(storage_entry) for storage_entry in self.item_config.values() if storage_entry.enabled)) def load_yaml_config(self) -> None: """Loads the YAML configuration to the storage""" for key in tuple(self.item_config.keys()): if self.item_config[key].yaml: del self.item_config[key] for yaml_entry in self.yaml_cfg: storage_entry = yaml_entry_to_storage_entry(yaml_entry) self.item_config[storage_entry.unique_identifier] = storage_entry self.storage.schedule_save(self.item_config) def get_storage_entry(self, unique_identifier: str) -> Optional[StorageEntry]: """Returns the StorageEntry for a unique_identifier""" return self.item_config.get(unique_identifier) def update_storage_entry(self, entry: StorageEntry) -> None: """Updates a config storage entry""" self.item_config[entry.unique_identifier] = entry self.storage.schedule_save(self.item_config) def _load_items(self, data: dict) -> dict: # pylint: disable=no-self-use entries = {} for entry in data: entries[entry["unique_identifier"]] = StorageEntry(**entry) return entries def _dump_items( # pylint: disable=no-self-use self, data: dict) -> List[Dict[str, Any]]: return [asdict(entry) for entry in data.values()] async def add_from_module(self, mod_obj: Module) -> None: """ Adds the item specifications of a module to the dict of available ones mod_obj: homecontrol.entity_types.Module """ for attribute in dir(mod_obj.mod): item_class = getattr(mod_obj.mod, attribute) if (isclass(item_class) and issubclass(item_class, Item) and item_class is not Item and item_class.__module__ == mod_obj.name): item_class.module = mod_obj item_class.type = f"{mod_obj.name}.{item_class.__name__}" self.item_constructors[ item_class.type] = item_class.constructor def iter_items_by_id(self, iterable) -> Iterator[Item]: """Translates item identifiers into item instances""" for identifier in iterable: if identifier in self.items: yield self.items[identifier] def get_by_unique_identifier(self, unique_identifier: str) -> Optional[Item]: """Returns an item by its unique identifier""" for item in self.items.values(): if item.unique_identifier == unique_identifier: return item def get_item(self, identifier: str) -> Optional[Item]: """Returns an item by identifier or unique_identifier""" return (self.items.get(identifier, None) or self.get_by_unique_identifier(identifier)) async def stop_item(self, item: Item, status: ItemStatus = ItemStatus.STOPPED) -> None: """Stops an item""" await item.stop() LOGGER.info("Item %s has been stopped with status %s", item.identifier, status) item.status = status async def stop(self) -> None: """Removes every item""" await asyncio.gather(*(self.remove_item(identifier) for identifier in self.items)) async def remove_item(self, identifier: str) -> None: """ Removes a HomeControl item identifier: str The item's identifier """ item = self.items.pop(identifier) if not item: LOGGER.info("Item %s does not exist so it could not be removed", identifier) return await self.stop_item(item) self.core.event_bus.broadcast(EVENT_ITEM_REMOVED, item=item) LOGGER.info("Item %s has been removed", identifier) async def create_from_storage_entry( self, storage_entry: StorageEntry) -> Optional[Item]: """Creates an Item from a storage entry""" try: if not storage_entry.provider: return await self.create_item( identifier=storage_entry.identifier, unique_identifier=storage_entry.unique_identifier, name=storage_entry.name, item_type=storage_entry.type, cfg=storage_entry.cfg, state_defaults=storage_entry.state_defaults) provider = cast( ItemProvider, self.core.module_manager.get_module(storage_entry.provider)) return await provider.create_item(storage_entry) except Exception: # pylint: disable=broad-except LOGGER.error("Could not create item %s", storage_entry.identifier, exc_info=True) async def init_item(self, item: Item) -> None: """Initialises an item""" LOGGER.debug("Initialising item %s", item.identifier) try: init_result = await item.init() except Exception: # pylint: disable=broad-except LOGGER.warning("An exception was raised when initialising item %s", item.identifier, exc_info=True) init_result = False # pylint: disable=singleton-comparison if init_result == False: # noqa: E712 item.status = ItemStatus.OFFLINE return item.status = ItemStatus.ONLINE async def register_entry(self, storage_entry: StorageEntry, override: bool = False) -> Optional[Item]: """Registers a storage entry""" existing_item = self.get_by_unique_identifier( storage_entry.unique_identifier) existing_entry = self.item_config.get(storage_entry.unique_identifier) if not override and existing_entry: return existing_item if existing_item: await self.remove_item(existing_item.identifier) storage_entry.enabled = bool(existing_item) self.item_config[storage_entry.unique_identifier] = storage_entry self.storage.schedule_save(self.item_config) if storage_entry.enabled: return await self.create_from_storage_entry(storage_entry) # pylint: disable=too-many-arguments,too-many-locals async def create_item(self, identifier: str, item_type: str, cfg: dict = None, state_defaults: dict = None, name: str = None, unique_identifier: str = None) -> Optional[Item]: """Creates a HomeControl item""" if item_type not in self.item_constructors: LOGGER.error("Item type not found: %s", item_type) return item_constructor = self.item_constructors[item_type] unique_identifier = unique_identifier or identifier item = await item_constructor(identifier, name, cfg, state_defaults=state_defaults, core=self.core, unique_identifier=unique_identifier) await self.register_item(item) return item async def register_item(self, item: Item) -> None: """Registers and initialises an already HomeControl item""" self.items[item.identifier] = item await self.init_item(item) self.core.event_bus.broadcast(EVENT_ITEM_CREATED, item=item) LOGGER.debug("Item registered: %s", item.unique_identifier) if item.status != ItemStatus.ONLINE: LOGGER.warning("Item could not be initialised: %s [%s]", item.unique_identifier, item.type) self.core.event_bus.broadcast(EVENT_ITEM_NOT_WORKING, item=item)
class Spotify(MediaPlayer): """A Spotify item""" module: "Module" token: Dict[str, Any] refresh_handle: asyncio.TimerHandle update_task: asyncio.Task config_schema = vol.Schema({ vol.Required("refresh_token"): str, vol.Required("scope"): str, vol.Optional("access_token"): str, vol.Optional("token_type"): str, vol.Optional("expires_in"): int, vol.Optional("expires_at"): int, }) async def init(self) -> None: self.auth = self.module.auth self.storage = Storage(f"item_data/{self.unique_identifier}", 1, core=self.core, storage_init=lambda: {}) self.token = self.storage.load_data() or self.cfg self._keep_access_token_new() self.update_task = self.core.loop.create_task( self._keep_states_updated()) self.api = spotipy.Spotify(client_credentials_manager=self) async def stop(self) -> None: self.refresh_handle.cancel() self.update_task.cancel() def get_access_token(self) -> Dict[str, Any]: """Returns the access token for spotipy""" return self.token["access_token"] def _keep_access_token_new(self) -> None: if (not self.token or self.token.get("expires_at", 0) < time.time() + 600): LOGGER.info("Requesting new access token") self.token = self.auth.refresh_access_token( self.cfg["refresh_token"]) self.storage.schedule_save(self.token) next_call = (self.core.loop.time() + self.token["expires_at"] - time.time() - 60) self.refresh_handle = self.core.loop.call_at( next_call, self._keep_access_token_new) async def _keep_states_updated(self) -> None: while True: await self._update_states() await asyncio.sleep(2) async def _update_states(self) -> None: # pylint: disable=protected-access playback = await self.core.loop.run_in_executor( None, partial(self.api._get, "me/player", additional_types="track,episode")) if not playback or not playback.get("item"): return self.states.bulk_update(playing=False, volume=0, title=None, artist=None, album=None, position=None, duration=None, cover=None) item = playback["item"] device = playback["device"] state_data = { "playing": playback["is_playing"], "volume": device.get("volume_percent", 0), "title": item["name"], "position": playback["progress_ms"] / 1000, "duration": item["duration_ms"] / 1000 } if item["type"] == "track": images = item["album"]["images"] state_data.update({ "artist": ", ".join(artist["name"] for artist in item.get("artists", [])), "album": item["album"]["name"], "cover": images[0]["url"] if images else None }) elif item["type"] == "episode": images = item["images"] state_data.update({ "artist": item["show"]["publisher"], "album": item["show"]["name"], "cover": images[0]["url"] if images else None }) self.states.bulk_update(**state_data) async def set_playing(self, playing: bool) -> Dict[str, Any]: await self.core.loop.run_in_executor( None, self.api.start_playback if playing else self.api.pause_playback) return {"playing": playing} async def set_volume(self, volume: int) -> Dict[str, Any]: await self.core.loop.run_in_executor(None, self.api.volume, volume) return {"volume": volume} async def set_position(self, position: int) -> Dict[str, Any]: await self.core.loop.run_in_executor(None, self.api.seek_track, position * 1000) return {"position": position} @action("play") async def action_play(self): await self.states.set("playing", True) @action("pause") async def action_pause(self): await self.states.set("playing", False) @action("stop") async def action_stop(self): await self.states.set("playing", False) @action("next") async def action_next(self): await self.core.loop.run_in_executor(None, self.api.next_track) @action("previous") async def action_previous(self): await self.core.loop.run_in_executor(None, self.api.previous_track)