import asyncio import aiofiles import json import logging import os.path import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, PLATFORM_SCHEMA) from homeassistant.components.media_player.const import ( MediaPlayerEntityFeature, MediaType) from homeassistant.const import ( CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity from . import COMPONENT_ABS_DIR, Helper from .controller import get_controller _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Infrarize Media Player" DEFAULT_DEVICE_CLASS = "tv" DEFAULT_DELAY = 0.5 CONF_UNIQUE_ID = 'unique_id' CONF_DEVICE_CODE = 'device_code' CONF_CONTROLLER_DATA = "controller_data" CONF_DELAY = "delay" CONF_POWER_SENSOR = 'power_sensor' CONF_SOURCE_NAMES = 'source_names' CONF_DEVICE_CLASS = 'device_class' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_DEVICE_CODE): cv.positive_int, vol.Required(CONF_CONTROLLER_DATA): cv.string, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.string, vol.Optional(CONF_POWER_SENSOR): cv.entity_id, vol.Optional(CONF_SOURCE_NAMES): dict, vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): cv.string }) async def async_setup_entry(hass, entry, async_add_entities): """Set up Infrarize Media Player from a config entry.""" config = {**entry.data, **entry.options} config.pop('platform', None) config.setdefault('unique_id', entry.unique_id) await async_setup_platform(hass, config, async_add_entities) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the IR Media Player platform.""" device_code = config.get(CONF_DEVICE_CODE) device_files_subdir = os.path.join('codes', 'media_player') device_files_absdir = os.path.join(COMPONENT_ABS_DIR, device_files_subdir) if not os.path.isdir(device_files_absdir): os.makedirs(device_files_absdir) device_json_filename = str(device_code) + '.json' device_json_path = os.path.join(device_files_absdir, device_json_filename) if not os.path.exists(device_json_path): _LOGGER.warning("Couldn't find the device Json file. The component will " \ "try to download it from the GitHub repo.") try: codes_source = ("https://raw.githubusercontent.com/" "kobimx/Infrarize/master/" "codes/media_player/{}.json") await Helper.downloader(codes_source.format(device_code), device_json_path) except Exception: _LOGGER.error("There was an error while downloading the device Json file. " \ "Please check your internet connection or if the device code " \ "exists on GitHub. If the problem still exists please " \ "place the file manually in the proper directory.") return try: async with aiofiles.open(device_json_path, mode='r') as j: _LOGGER.debug(f"loading json file {device_json_path}") content = await j.read() device_data = json.loads(content) _LOGGER.debug(f"{device_json_path} file loaded") except Exception: _LOGGER.error("The device JSON file is invalid") return async_add_entities([InfrarizeMediaPlayer( hass, config, device_data )]) class InfrarizeMediaPlayer(MediaPlayerEntity, RestoreEntity): def __init__(self, hass, config, device_data): self.hass = hass self._unique_id = config.get(CONF_UNIQUE_ID) self._name = config.get(CONF_NAME) self._device_code = config.get(CONF_DEVICE_CODE) self._controller_data = config.get(CONF_CONTROLLER_DATA) self._delay = config.get(CONF_DELAY) self._power_sensor = config.get(CONF_POWER_SENSOR) self._manufacturer = device_data['manufacturer'] self._supported_models = device_data['supportedModels'] self._supported_controller = device_data['supportedController'] self._commands_encoding = device_data['commandsEncoding'] self._commands = device_data['commands'] self._state = STATE_OFF self._sources_list = [] self._source = None self._support_flags = 0 self._device_class = config.get(CONF_DEVICE_CLASS) #Supported features if 'off' in self._commands and self._commands['off'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.TURN_OFF if 'on' in self._commands and self._commands['on'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.TURN_ON if 'previousChannel' in self._commands and self._commands['previousChannel'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.PREVIOUS_TRACK if 'nextChannel' in self._commands and self._commands['nextChannel'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.NEXT_TRACK if ('volumeDown' in self._commands and self._commands['volumeDown'] is not None) \ or ('volumeUp' in self._commands and self._commands['volumeUp'] is not None): self._support_flags = self._support_flags | MediaPlayerEntityFeature.VOLUME_STEP if 'mute' in self._commands and self._commands['mute'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.VOLUME_MUTE if 'sources' in self._commands and self._commands['sources'] is not None: self._support_flags = self._support_flags | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.PLAY_MEDIA for source, new_name in config.get(CONF_SOURCE_NAMES, {}).items(): if source in self._commands['sources']: if new_name is not None: self._commands['sources'][new_name] = self._commands['sources'][source] del self._commands['sources'][source] #Sources list for key in self._commands['sources']: self._sources_list.append(key) self._temp_lock = asyncio.Lock() #Init the IR/RF controller self._controller = get_controller( self.hass, self._supported_controller, self._commands_encoding, self._controller_data, self._delay) async def async_added_to_hass(self): """Run when entity about to be added.""" await super().async_added_to_hass() last_state = await self.async_get_last_state() if last_state is not None: self._state = last_state.state @property def should_poll(self): """Push an update after each command.""" return True @property def unique_id(self): """Return a unique ID.""" return self._unique_id @property def name(self): """Return the name of the media player.""" return self._name @property def device_class(self): """Return the device_class of the media player.""" return self._device_class @property def state(self): """Return the state of the player.""" return self._state @property def media_title(self): """Return the title of current playing media.""" return None @property def media_content_type(self): """Content type of current playing media.""" return MediaType.CHANNEL @property def source_list(self): return self._sources_list @property def source(self): return self._source @property def supported_features(self): """Flag media player features that are supported.""" return self._support_flags @property def extra_state_attributes(self): """Platform specific attributes.""" return { 'device_code': self._device_code, 'manufacturer': self._manufacturer, 'supported_models': self._supported_models, 'supported_controller': self._supported_controller, 'commands_encoding': self._commands_encoding, } async def async_turn_off(self): """Turn the media player off.""" await self.send_command(self._commands['off']) if self._power_sensor is None: self._state = STATE_OFF self._source = None self.async_write_ha_state() async def async_turn_on(self): """Turn the media player off.""" await self.send_command(self._commands['on']) if self._power_sensor is None: self._state = STATE_ON self.async_write_ha_state() async def async_media_previous_track(self): """Send previous track command.""" await self.send_command(self._commands['previousChannel']) self.async_write_ha_state() async def async_media_next_track(self): """Send next track command.""" await self.send_command(self._commands['nextChannel']) self.async_write_ha_state() async def async_volume_down(self): """Turn volume down for media player.""" await self.send_command(self._commands['volumeDown']) self.async_write_ha_state() async def async_volume_up(self): """Turn volume up for media player.""" await self.send_command(self._commands['volumeUp']) self.async_write_ha_state() async def async_mute_volume(self, mute): """Mute the volume.""" await self.send_command(self._commands['mute']) self.async_write_ha_state() async def async_select_source(self, source): """Select channel from source.""" self._source = source await self.send_command(self._commands['sources'][source]) self.async_write_ha_state() async def async_play_media(self, media_type, media_id, **kwargs): """Support channel change through play_media service.""" if self._state == STATE_OFF: await self.async_turn_on() if media_type != MediaType.CHANNEL: _LOGGER.error("invalid media type") return if not media_id.isdigit(): _LOGGER.error("media_id must be a channel number") return self._source = "Channel {}".format(media_id) for digit in media_id: await self.send_command(self._commands['sources']["Channel {}".format(digit)]) self.async_write_ha_state() async def send_command(self, command): async with self._temp_lock: try: await self._controller.send(command) except Exception as e: _LOGGER.exception(e) async def async_update(self): if self._power_sensor is None: return power_state = self.hass.states.get(self._power_sensor) if power_state: if power_state.state == STATE_OFF: self._state = STATE_OFF self._source = None elif power_state.state == STATE_ON: self._state = STATE_ON