Example #1
0
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)
Example #2
0
    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
Example #3
0
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)
Example #4
0
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)