init
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import aiofiles
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import binascii
|
||||
from distutils.version import StrictVersion
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
import requests
|
||||
import struct
|
||||
import voluptuous as vol
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME, __version__ as current_ha_version)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'smartir'
|
||||
VERSION = '1.18.1'
|
||||
MANIFEST_URL = (
|
||||
"https://raw.githubusercontent.com/"
|
||||
"smartHomeHub/SmartIR/{}/"
|
||||
"custom_components/smartir/manifest.json")
|
||||
REMOTE_BASE_URL = (
|
||||
"https://raw.githubusercontent.com/"
|
||||
"smartHomeHub/SmartIR/{}/"
|
||||
"custom_components/smartir/")
|
||||
COMPONENT_ABS_DIR = os.path.dirname(
|
||||
os.path.abspath(__file__))
|
||||
|
||||
CONF_CHECK_UPDATES = 'check_updates'
|
||||
CONF_UPDATE_BRANCH = 'update_branch'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_CHECK_UPDATES, default=True): cv.boolean,
|
||||
vol.Optional(CONF_UPDATE_BRANCH, default='master'): vol.In(
|
||||
['master', 'rc'])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the SmartIR component."""
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
if conf is None:
|
||||
return True
|
||||
|
||||
check_updates = conf[CONF_CHECK_UPDATES]
|
||||
update_branch = conf[CONF_UPDATE_BRANCH]
|
||||
|
||||
async def _check_updates(service):
|
||||
await _update(hass, update_branch)
|
||||
|
||||
async def _update_component(service):
|
||||
await _update(hass, update_branch, True)
|
||||
|
||||
hass.services.async_register(DOMAIN, 'check_updates', _check_updates)
|
||||
hass.services.async_register(DOMAIN, 'update_component', _update_component)
|
||||
|
||||
if check_updates:
|
||||
await _update(hass, update_branch, False, False)
|
||||
|
||||
return True
|
||||
|
||||
async def _update(hass, branch, do_update=False, notify_if_latest=True):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(MANIFEST_URL.format(branch)) as response:
|
||||
if response.status == 200:
|
||||
|
||||
data = await response.json(content_type='text/plain')
|
||||
min_ha_version = data['homeassistant']
|
||||
last_version = data['updater']['version']
|
||||
release_notes = data['updater']['releaseNotes']
|
||||
|
||||
if StrictVersion(last_version) <= StrictVersion(VERSION):
|
||||
if notify_if_latest:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"You're already using the latest version!",
|
||||
title='SmartIR')
|
||||
return
|
||||
|
||||
if StrictVersion(current_ha_version) < StrictVersion(min_ha_version):
|
||||
hass.components.persistent_notification.async_create(
|
||||
"There is a new version of SmartIR integration, but it is **incompatible** "
|
||||
"with your system. Please first update Home Assistant.", title='SmartIR')
|
||||
return
|
||||
|
||||
if do_update is False:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"A new version of SmartIR integration is available ({}). "
|
||||
"Call the ``smartir.update_component`` service to update "
|
||||
"the integration. \n\n **Release notes:** \n{}"
|
||||
.format(last_version, release_notes), title='SmartIR')
|
||||
return
|
||||
|
||||
# Begin update
|
||||
files = data['updater']['files']
|
||||
has_errors = False
|
||||
|
||||
for file in files:
|
||||
try:
|
||||
source = REMOTE_BASE_URL.format(branch) + file
|
||||
dest = os.path.join(COMPONENT_ABS_DIR, file)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
await Helper.downloader(source, dest)
|
||||
except Exception:
|
||||
has_errors = True
|
||||
_LOGGER.error("Error updating %s. Please update the file manually.", file)
|
||||
|
||||
if has_errors:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"There was an error updating one or more files of SmartIR. "
|
||||
"Please check the logs for more information.", title='SmartIR')
|
||||
else:
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Successfully updated to {}. Please restart Home Assistant."
|
||||
.format(last_version), title='SmartIR')
|
||||
except Exception:
|
||||
_LOGGER.error("An error occurred while checking for updates.")
|
||||
|
||||
class Helper():
|
||||
@staticmethod
|
||||
async def downloader(source, dest):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(source) as response:
|
||||
if response.status == 200:
|
||||
async with aiofiles.open(dest, mode='wb') as f:
|
||||
await f.write(await response.read())
|
||||
else:
|
||||
raise Exception("File not found")
|
||||
|
||||
@staticmethod
|
||||
def pronto2lirc(pronto):
|
||||
codes = [int(binascii.hexlify(pronto[i:i+2]), 16) for i in range(0, len(pronto), 2)]
|
||||
|
||||
if codes[0]:
|
||||
raise ValueError("Pronto code should start with 0000")
|
||||
if len(codes) != 4 + 2 * (codes[2] + codes[3]):
|
||||
raise ValueError("Number of pulse widths does not match the preamble")
|
||||
|
||||
frequency = 1 / (codes[1] * 0.241246)
|
||||
return [int(round(code / frequency)) for code in codes[4:]]
|
||||
|
||||
@staticmethod
|
||||
def lirc2broadlink(pulses):
|
||||
array = bytearray()
|
||||
|
||||
for pulse in pulses:
|
||||
pulse = int(pulse * 269 / 8192)
|
||||
|
||||
if pulse < 256:
|
||||
array += bytearray(struct.pack('>B', pulse))
|
||||
else:
|
||||
array += bytearray([0x00])
|
||||
array += bytearray(struct.pack('>H', pulse))
|
||||
|
||||
packet = bytearray([0x26, 0x00])
|
||||
packet += bytearray(struct.pack('<H', len(array)))
|
||||
packet += array
|
||||
packet += bytearray([0x0d, 0x05])
|
||||
|
||||
# Add 0s to make ultimate packet size a multiple of 16 for 128-bit AES encryption.
|
||||
remainder = (len(packet) + 4) % 16
|
||||
if remainder:
|
||||
packet += bytearray(16 - remainder)
|
||||
return packet
|
||||
@@ -0,0 +1,454 @@
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity, PLATFORM_SCHEMA
|
||||
from homeassistant.components.climate.const import (
|
||||
ClimateEntityFeature, HVACMode, HVAC_MODES, ATTR_HVAC_MODE)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE, ATTR_TEMPERATURE,
|
||||
PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE)
|
||||
from homeassistant.core import Event, EventStateChangedData, callback
|
||||
from homeassistant.helpers.event import async_track_state_change, async_track_state_change_event
|
||||
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 = "SmartIR Climate"
|
||||
DEFAULT_DELAY = 0.5
|
||||
|
||||
CONF_UNIQUE_ID = 'unique_id'
|
||||
CONF_DEVICE_CODE = 'device_code'
|
||||
CONF_CONTROLLER_DATA = "controller_data"
|
||||
CONF_DELAY = "delay"
|
||||
CONF_TEMPERATURE_SENSOR = 'temperature_sensor'
|
||||
CONF_HUMIDITY_SENSOR = 'humidity_sensor'
|
||||
CONF_POWER_SENSOR = 'power_sensor'
|
||||
CONF_POWER_SENSOR_RESTORE_STATE = 'power_sensor_restore_state'
|
||||
|
||||
SUPPORT_FLAGS = (
|
||||
ClimateEntityFeature.TURN_OFF |
|
||||
ClimateEntityFeature.TURN_ON |
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE |
|
||||
ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
|
||||
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.positive_float,
|
||||
vol.Optional(CONF_TEMPERATURE_SENSOR): cv.entity_id,
|
||||
vol.Optional(CONF_HUMIDITY_SENSOR): cv.entity_id,
|
||||
vol.Optional(CONF_POWER_SENSOR): cv.entity_id,
|
||||
vol.Optional(CONF_POWER_SENSOR_RESTORE_STATE, default=False): cv.boolean
|
||||
})
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the IR Climate platform."""
|
||||
_LOGGER.debug("Setting up the smartir platform")
|
||||
device_code = config.get(CONF_DEVICE_CODE)
|
||||
device_files_subdir = os.path.join('codes', 'climate')
|
||||
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/"
|
||||
"smartHomeHub/SmartIR/master/"
|
||||
"codes/climate/{}.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([SmartIRClimate(
|
||||
hass, config, device_data
|
||||
)])
|
||||
|
||||
class SmartIRClimate(ClimateEntity, RestoreEntity):
|
||||
def __init__(self, hass, config, device_data):
|
||||
_LOGGER.debug(f"SmartIRClimate init started for device {config.get(CONF_NAME)} supported models {device_data['supportedModels']}")
|
||||
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._temperature_sensor = config.get(CONF_TEMPERATURE_SENSOR)
|
||||
self._humidity_sensor = config.get(CONF_HUMIDITY_SENSOR)
|
||||
self._power_sensor = config.get(CONF_POWER_SENSOR)
|
||||
self._power_sensor_restore_state = config.get(CONF_POWER_SENSOR_RESTORE_STATE)
|
||||
|
||||
self._manufacturer = device_data['manufacturer']
|
||||
self._supported_models = device_data['supportedModels']
|
||||
self._supported_controller = device_data['supportedController']
|
||||
self._commands_encoding = device_data['commandsEncoding']
|
||||
self._min_temperature = device_data['minTemperature']
|
||||
self._max_temperature = device_data['maxTemperature']
|
||||
self._precision = device_data['precision']
|
||||
|
||||
valid_hvac_modes = [x for x in device_data['operationModes'] if x in HVAC_MODES]
|
||||
|
||||
self._operation_modes = [HVACMode.OFF] + valid_hvac_modes
|
||||
self._fan_modes = device_data['fanModes']
|
||||
self._swing_modes = device_data.get('swingModes')
|
||||
self._commands = device_data['commands']
|
||||
|
||||
self._target_temperature = self._min_temperature
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
self._current_fan_mode = self._fan_modes[0]
|
||||
self._current_swing_mode = None
|
||||
self._last_on_operation = None
|
||||
|
||||
self._current_temperature = None
|
||||
self._current_humidity = None
|
||||
|
||||
self._unit = hass.config.units.temperature_unit
|
||||
|
||||
#Supported features
|
||||
self._support_flags = SUPPORT_FLAGS
|
||||
self._support_swing = False
|
||||
|
||||
if self._swing_modes:
|
||||
self._support_flags = self._support_flags | ClimateEntityFeature.SWING_MODE
|
||||
self._current_swing_mode = self._swing_modes[0]
|
||||
self._support_swing = True
|
||||
|
||||
self._temp_lock = asyncio.Lock()
|
||||
self._on_by_remote = False
|
||||
|
||||
#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()
|
||||
_LOGGER.debug(f"async_added_to_hass {self} {self.name} {self.supported_features}")
|
||||
|
||||
last_state = await self.async_get_last_state()
|
||||
|
||||
if last_state is not None:
|
||||
self._hvac_mode = last_state.state
|
||||
self._current_fan_mode = last_state.attributes['fan_mode']
|
||||
self._current_swing_mode = last_state.attributes.get('swing_mode')
|
||||
self._target_temperature = last_state.attributes['temperature']
|
||||
|
||||
if 'last_on_operation' in last_state.attributes:
|
||||
self._last_on_operation = last_state.attributes['last_on_operation']
|
||||
|
||||
if self._temperature_sensor:
|
||||
async_track_state_change_event(self.hass, self._temperature_sensor,
|
||||
self._async_temp_sensor_changed)
|
||||
|
||||
temp_sensor_state = self.hass.states.get(self._temperature_sensor)
|
||||
if temp_sensor_state and temp_sensor_state.state != STATE_UNKNOWN:
|
||||
self._async_update_temp(temp_sensor_state)
|
||||
|
||||
if self._humidity_sensor:
|
||||
async_track_state_change_event(self.hass, self._humidity_sensor,
|
||||
self._async_humidity_sensor_changed)
|
||||
|
||||
humidity_sensor_state = self.hass.states.get(self._humidity_sensor)
|
||||
if humidity_sensor_state and humidity_sensor_state.state != STATE_UNKNOWN:
|
||||
self._async_update_humidity(humidity_sensor_state)
|
||||
|
||||
if self._power_sensor:
|
||||
async_track_state_change_event(self.hass, self._power_sensor,
|
||||
self._async_power_sensor_changed)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the climate device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
if self.hvac_mode != HVACMode.OFF:
|
||||
return self.hvac_mode
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the polling state."""
|
||||
return self._min_temperature
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the polling state."""
|
||||
return self._max_temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self._precision
|
||||
|
||||
@property
|
||||
def hvac_modes(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return self._operation_modes
|
||||
|
||||
@property
|
||||
def hvac_mode(self):
|
||||
"""Return hvac mode ie. heat, cool."""
|
||||
return self._hvac_mode
|
||||
|
||||
@property
|
||||
def last_on_operation(self):
|
||||
"""Return the last non-idle operation ie. heat, cool."""
|
||||
return self._last_on_operation
|
||||
|
||||
@property
|
||||
def fan_modes(self):
|
||||
"""Return the list of available fan modes."""
|
||||
return self._fan_modes
|
||||
|
||||
@property
|
||||
def fan_mode(self):
|
||||
"""Return the fan setting."""
|
||||
return self._current_fan_mode
|
||||
|
||||
@property
|
||||
def swing_modes(self):
|
||||
"""Return the swing modes currently supported for this device."""
|
||||
return self._swing_modes
|
||||
|
||||
@property
|
||||
def swing_mode(self):
|
||||
"""Return the current swing mode."""
|
||||
return self._current_swing_mode
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temperature
|
||||
|
||||
@property
|
||||
def current_humidity(self):
|
||||
"""Return the current humidity."""
|
||||
return self._current_humidity
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Platform specific attributes."""
|
||||
return {
|
||||
'last_on_operation': self._last_on_operation,
|
||||
'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_set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE)
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
|
||||
if temperature is None:
|
||||
return
|
||||
|
||||
if temperature < self._min_temperature or temperature > self._max_temperature:
|
||||
_LOGGER.warning('The temperature value is out of min/max range')
|
||||
return
|
||||
|
||||
if self._precision == PRECISION_WHOLE:
|
||||
self._target_temperature = round(temperature)
|
||||
else:
|
||||
self._target_temperature = round(temperature, 1)
|
||||
|
||||
if hvac_mode:
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
return
|
||||
|
||||
if not self._hvac_mode.lower() == HVACMode.OFF:
|
||||
await self.send_command()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode):
|
||||
"""Set operation mode."""
|
||||
self._hvac_mode = hvac_mode
|
||||
|
||||
if not hvac_mode == HVACMode.OFF:
|
||||
self._last_on_operation = hvac_mode
|
||||
|
||||
await self.send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode):
|
||||
"""Set fan mode."""
|
||||
self._current_fan_mode = fan_mode
|
||||
|
||||
if not self._hvac_mode.lower() == HVACMode.OFF:
|
||||
await self.send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_mode(self, swing_mode):
|
||||
"""Set swing mode."""
|
||||
self._current_swing_mode = swing_mode
|
||||
|
||||
if not self._hvac_mode.lower() == HVACMode.OFF:
|
||||
await self.send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
async def async_turn_on(self):
|
||||
"""Turn on."""
|
||||
if self._last_on_operation is not None:
|
||||
await self.async_set_hvac_mode(self._last_on_operation)
|
||||
else:
|
||||
await self.async_set_hvac_mode(self._operation_modes[1])
|
||||
|
||||
async def send_command(self):
|
||||
async with self._temp_lock:
|
||||
try:
|
||||
self._on_by_remote = False
|
||||
operation_mode = self._hvac_mode
|
||||
fan_mode = self._current_fan_mode
|
||||
swing_mode = self._current_swing_mode
|
||||
target_temperature = '{0:g}'.format(self._target_temperature)
|
||||
|
||||
if operation_mode.lower() == HVACMode.OFF:
|
||||
await self._controller.send(self._commands['off'])
|
||||
return
|
||||
|
||||
if 'on' in self._commands:
|
||||
await self._controller.send(self._commands['on'])
|
||||
await asyncio.sleep(self._delay)
|
||||
|
||||
if self._support_swing == True:
|
||||
await self._controller.send(
|
||||
self._commands[operation_mode][fan_mode][swing_mode][target_temperature])
|
||||
else:
|
||||
await self._controller.send(
|
||||
self._commands[operation_mode][fan_mode][target_temperature])
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.exception(e)
|
||||
|
||||
@callback
|
||||
async def _async_temp_sensor_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle temperature sensor changes."""
|
||||
new_state = event.data["new_state"]
|
||||
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._async_update_temp(new_state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
async def _async_humidity_sensor_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle humidity sensor changes."""
|
||||
new_state = event.data["new_state"]
|
||||
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
self._async_update_humidity(new_state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
entity_id = event.data["entity_id"]
|
||||
old_state = event.data["old_state"]
|
||||
new_state = event.data["new_state"]
|
||||
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if old_state is not None and new_state.state == old_state.state:
|
||||
return
|
||||
|
||||
if new_state.state == STATE_ON and self._hvac_mode == HVACMode.OFF:
|
||||
self._on_by_remote = True
|
||||
if self._power_sensor_restore_state == True and self._last_on_operation is not None:
|
||||
self._hvac_mode = self._last_on_operation
|
||||
else:
|
||||
self._hvac_mode = STATE_ON
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
if new_state.state == STATE_OFF:
|
||||
self._on_by_remote = False
|
||||
if self._hvac_mode != HVACMode.OFF:
|
||||
self._hvac_mode = HVACMode.OFF
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_temp(self, state):
|
||||
"""Update thermostat with latest state from temperature sensor."""
|
||||
try:
|
||||
if state.state != STATE_UNKNOWN and state.state != STATE_UNAVAILABLE:
|
||||
self._current_temperature = float(state.state)
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update from temperature sensor: %s", ex)
|
||||
|
||||
@callback
|
||||
def _async_update_humidity(self, state):
|
||||
"""Update thermostat with latest state from humidity sensor."""
|
||||
try:
|
||||
if state.state != STATE_UNKNOWN and state.state != STATE_UNAVAILABLE:
|
||||
self._current_humidity = float(state.state)
|
||||
except ValueError as ex:
|
||||
_LOGGER.error("Unable to update from humidity sensor: %s", ex)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
"""Constants for the SmartIR config flow and platforms."""
|
||||
from __future__ import annotations
|
||||
|
||||
# ── Sentinels ──────────────────────────────────────────────────────────────────
|
||||
MANUAL_ENTRY_SENTINEL = "__manual__"
|
||||
ALL_CONTROLLERS_SENTINEL = "Any"
|
||||
INDEX_FILENAME = "codes_index.json"
|
||||
|
||||
# ── Config-entry data keys ─────────────────────────────────────────────────────
|
||||
CONF_PLATFORM = "platform"
|
||||
CONF_DEVICE_CODE = "device_code"
|
||||
CONF_CONTROLLER_DATA = "controller_data"
|
||||
CONF_DELAY = "delay"
|
||||
CONF_DEVICE_NAME = "name"
|
||||
CONF_TEMPERATURE_SENSOR = "temperature_sensor"
|
||||
CONF_HUMIDITY_SENSOR = "humidity_sensor"
|
||||
CONF_POWER_SENSOR = "power_sensor"
|
||||
CONF_POWER_SENSOR_RESTORE_STATE = "power_sensor_restore_state"
|
||||
CONF_DEVICE_CLASS = "device_class"
|
||||
CONF_SOURCE_NAMES = "source_names"
|
||||
|
||||
# ── Defaults ───────────────────────────────────────────────────────────────────
|
||||
DEFAULT_DELAY = "0.5"
|
||||
|
||||
# ── Platform choices ───────────────────────────────────────────────────────────
|
||||
PLATFORMS: dict[str, str] = {
|
||||
"climate": "Climate (Air Conditioner / Heat Pump)",
|
||||
"fan": "Fan",
|
||||
"media_player": "Media Player (TV / Receiver)",
|
||||
"light": "Light",
|
||||
}
|
||||
|
||||
PLATFORM_SUBDIR: dict[str, str] = {
|
||||
"climate": "climate",
|
||||
"fan": "fan",
|
||||
"media_player": "media_player",
|
||||
"light": "light",
|
||||
}
|
||||
|
||||
DEFAULT_DEVICE_NAMES: dict[str, str] = {
|
||||
"climate": "SmartIR Climate",
|
||||
"fan": "SmartIR Fan",
|
||||
"media_player": "SmartIR Media Player",
|
||||
"light": "SmartIR Light",
|
||||
}
|
||||
|
||||
# ── Controller metadata ────────────────────────────────────────────────────────
|
||||
# Controllers whose device identifier is a Home Assistant remote entity ID.
|
||||
# These get an EntitySelector in the UI instead of a plain text field.
|
||||
ENTITY_BASED_CONTROLLERS: frozenset[str] = frozenset({"Broadlink", "Xiaomi"})
|
||||
|
||||
CONTROLLER_HINTS: dict[str, str] = {
|
||||
"Broadlink": "Select the Broadlink remote entity that will send IR commands.",
|
||||
"Xiaomi": "Select the Xiaomi remote entity that will send IR commands.",
|
||||
"MQTT": "Enter the MQTT topic to publish IR commands to (e.g. zigbee2mqtt/ir_blaster/set).",
|
||||
"LOOKin": "Enter the LOOKin device IP address (e.g. 192.168.1.42).",
|
||||
"ESPHome": "Enter the ESPHome remote service name (e.g. esphome_ir_blaster).",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from base64 import b64encode
|
||||
import binascii
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from . import Helper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BROADLINK_CONTROLLER = 'Broadlink'
|
||||
XIAOMI_CONTROLLER = 'Xiaomi'
|
||||
MQTT_CONTROLLER = 'MQTT'
|
||||
LOOKIN_CONTROLLER = 'LOOKin'
|
||||
ESPHOME_CONTROLLER = 'ESPHome'
|
||||
|
||||
ENC_BASE64 = 'Base64'
|
||||
ENC_HEX = 'Hex'
|
||||
ENC_PRONTO = 'Pronto'
|
||||
ENC_RAW = 'Raw'
|
||||
|
||||
BROADLINK_COMMANDS_ENCODING = [ENC_BASE64, ENC_HEX, ENC_PRONTO]
|
||||
XIAOMI_COMMANDS_ENCODING = [ENC_PRONTO, ENC_RAW]
|
||||
MQTT_COMMANDS_ENCODING = [ENC_RAW]
|
||||
LOOKIN_COMMANDS_ENCODING = [ENC_PRONTO, ENC_RAW]
|
||||
ESPHOME_COMMANDS_ENCODING = [ENC_RAW]
|
||||
|
||||
|
||||
def get_controller(hass, controller, encoding, controller_data, delay):
|
||||
"""Return a controller compatible with the specification provided."""
|
||||
controllers = {
|
||||
BROADLINK_CONTROLLER: BroadlinkController,
|
||||
XIAOMI_CONTROLLER: XiaomiController,
|
||||
MQTT_CONTROLLER: MQTTController,
|
||||
LOOKIN_CONTROLLER: LookinController,
|
||||
ESPHOME_CONTROLLER: ESPHomeController
|
||||
}
|
||||
try:
|
||||
return controllers[controller](hass, controller, encoding, controller_data, delay)
|
||||
except KeyError:
|
||||
raise Exception("The controller is not supported.")
|
||||
|
||||
|
||||
class AbstractController(ABC):
|
||||
"""Representation of a controller."""
|
||||
def __init__(self, hass, controller, encoding, controller_data, delay):
|
||||
self.check_encoding(encoding)
|
||||
self.hass = hass
|
||||
self._controller = controller
|
||||
self._encoding = encoding
|
||||
self._controller_data = controller_data
|
||||
self._delay = delay
|
||||
|
||||
@abstractmethod
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
pass
|
||||
|
||||
|
||||
class BroadlinkController(AbstractController):
|
||||
"""Controls a Broadlink device."""
|
||||
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
if encoding not in BROADLINK_COMMANDS_ENCODING:
|
||||
raise Exception("The encoding is not supported "
|
||||
"by the Broadlink controller.")
|
||||
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
commands = []
|
||||
|
||||
if not isinstance(command, list):
|
||||
command = [command]
|
||||
|
||||
for _command in command:
|
||||
if self._encoding == ENC_HEX:
|
||||
try:
|
||||
_command = binascii.unhexlify(_command)
|
||||
_command = b64encode(_command).decode('utf-8')
|
||||
except:
|
||||
raise Exception("Error while converting "
|
||||
"Hex to Base64 encoding")
|
||||
|
||||
if self._encoding == ENC_PRONTO:
|
||||
try:
|
||||
_command = _command.replace(' ', '')
|
||||
_command = bytearray.fromhex(_command)
|
||||
_command = Helper.pronto2lirc(_command)
|
||||
_command = Helper.lirc2broadlink(_command)
|
||||
_command = b64encode(_command).decode('utf-8')
|
||||
except:
|
||||
raise Exception("Error while converting "
|
||||
"Pronto to Base64 encoding")
|
||||
|
||||
commands.append('b64:' + _command)
|
||||
|
||||
service_data = {
|
||||
ATTR_ENTITY_ID: self._controller_data,
|
||||
'command': commands,
|
||||
'delay_secs': self._delay
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
'remote', 'send_command', service_data)
|
||||
|
||||
|
||||
class XiaomiController(AbstractController):
|
||||
"""Controls a Xiaomi device."""
|
||||
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
if encoding not in XIAOMI_COMMANDS_ENCODING:
|
||||
raise Exception("The encoding is not supported "
|
||||
"by the Xiaomi controller.")
|
||||
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
service_data = {
|
||||
ATTR_ENTITY_ID: self._controller_data,
|
||||
'command': self._encoding.lower() + ':' + command
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
'remote', 'send_command', service_data)
|
||||
|
||||
|
||||
class MQTTController(AbstractController):
|
||||
"""Controls a MQTT device."""
|
||||
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
if encoding not in MQTT_COMMANDS_ENCODING:
|
||||
raise Exception("The encoding is not supported "
|
||||
"by the mqtt controller.")
|
||||
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
service_data = {
|
||||
'topic': self._controller_data,
|
||||
'payload': command
|
||||
}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
'mqtt', 'publish', service_data)
|
||||
|
||||
|
||||
class LookinController(AbstractController):
|
||||
"""Controls a Lookin device."""
|
||||
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
if encoding not in LOOKIN_COMMANDS_ENCODING:
|
||||
raise Exception("The encoding is not supported "
|
||||
"by the LOOKin controller.")
|
||||
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
encoding = self._encoding.lower().replace('pronto', 'prontohex')
|
||||
url = f"http://{self._controller_data}/commands/ir/" \
|
||||
f"{encoding}/{command}"
|
||||
await self.hass.async_add_executor_job(requests.get, url)
|
||||
|
||||
|
||||
class ESPHomeController(AbstractController):
|
||||
"""Controls a ESPHome device."""
|
||||
|
||||
def check_encoding(self, encoding):
|
||||
"""Check if the encoding is supported by the controller."""
|
||||
if encoding not in ESPHOME_COMMANDS_ENCODING:
|
||||
raise Exception("The encoding is not supported "
|
||||
"by the ESPHome controller.")
|
||||
|
||||
async def send(self, command):
|
||||
"""Send a command."""
|
||||
service_data = {'command': json.loads(command)}
|
||||
|
||||
await self.hass.services.async_call(
|
||||
'esphome', self._controller_data, service_data)
|
||||
@@ -0,0 +1,307 @@
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.fan import (
|
||||
FanEntity, FanEntityFeature,
|
||||
PLATFORM_SCHEMA, DIRECTION_REVERSE, DIRECTION_FORWARD)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
|
||||
from homeassistant.core import Event, EventStateChangedData, callback
|
||||
from homeassistant.helpers.event import async_track_state_change, async_track_state_change_event
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item
|
||||
)
|
||||
from . import COMPONENT_ABS_DIR, Helper
|
||||
from .controller import get_controller
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "SmartIR Fan"
|
||||
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'
|
||||
|
||||
SPEED_OFF = "off"
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the IR Fan platform."""
|
||||
device_code = config.get(CONF_DEVICE_CODE)
|
||||
device_files_subdir = os.path.join('codes', 'fan')
|
||||
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/"
|
||||
"smartHomeHub/SmartIR/master/"
|
||||
"codes/fan/{}.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([SmartIRFan(
|
||||
hass, config, device_data
|
||||
)])
|
||||
|
||||
class SmartIRFan(FanEntity, 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._speed_list = device_data['speed']
|
||||
self._commands = device_data['commands']
|
||||
|
||||
self._speed = SPEED_OFF
|
||||
self._direction = None
|
||||
self._last_on_speed = None
|
||||
self._oscillating = None
|
||||
self._support_flags = (
|
||||
FanEntityFeature.SET_SPEED
|
||||
| FanEntityFeature.TURN_OFF
|
||||
| FanEntityFeature.TURN_ON)
|
||||
|
||||
if (DIRECTION_REVERSE in self._commands and \
|
||||
DIRECTION_FORWARD in self._commands):
|
||||
self._direction = DIRECTION_REVERSE
|
||||
self._support_flags = (
|
||||
self._support_flags | FanEntityFeature.DIRECTION)
|
||||
if ('oscillate' in self._commands):
|
||||
self._oscillating = False
|
||||
self._support_flags = (
|
||||
self._support_flags | FanEntityFeature.OSCILLATE)
|
||||
|
||||
|
||||
self._temp_lock = asyncio.Lock()
|
||||
self._on_by_remote = False
|
||||
|
||||
#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:
|
||||
if 'speed' in last_state.attributes:
|
||||
self._speed = last_state.attributes['speed']
|
||||
|
||||
#If _direction has a value the direction controls appears
|
||||
#in UI even if SUPPORT_DIRECTION is not provided in the flags
|
||||
if ('direction' in last_state.attributes and \
|
||||
self._support_flags & FanEntityFeature.DIRECTION):
|
||||
self._direction = last_state.attributes['direction']
|
||||
|
||||
if 'last_on_speed' in last_state.attributes:
|
||||
self._last_on_speed = last_state.attributes['last_on_speed']
|
||||
|
||||
if self._power_sensor:
|
||||
async_track_state_change_event(self.hass, self._power_sensor,
|
||||
self._async_power_sensor_changed)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of the fan."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current state."""
|
||||
if (self._on_by_remote or \
|
||||
self._speed != SPEED_OFF):
|
||||
return STATE_ON
|
||||
return SPEED_OFF
|
||||
|
||||
@property
|
||||
def percentage(self):
|
||||
"""Return speed percentage of the fan."""
|
||||
if (self._speed == SPEED_OFF):
|
||||
return 0
|
||||
|
||||
return ordered_list_item_to_percentage(self._speed_list, self._speed)
|
||||
|
||||
@property
|
||||
def speed_count(self):
|
||||
"""Return the number of speeds the fan supports."""
|
||||
return len(self._speed_list)
|
||||
|
||||
@property
|
||||
def oscillating(self):
|
||||
"""Return the oscillation state."""
|
||||
return self._oscillating
|
||||
|
||||
@property
|
||||
def current_direction(self):
|
||||
"""Return the direction state."""
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def last_on_speed(self):
|
||||
"""Return the last non-idle speed."""
|
||||
return self._last_on_speed
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the list of supported features."""
|
||||
return self._support_flags
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
"""Platform specific attributes."""
|
||||
return {
|
||||
'last_on_speed': self._last_on_speed,
|
||||
'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_set_percentage(self, percentage: int):
|
||||
"""Set the desired speed for the fan."""
|
||||
if (percentage == 0):
|
||||
self._speed = SPEED_OFF
|
||||
else:
|
||||
self._speed = percentage_to_ordered_list_item(
|
||||
self._speed_list, percentage)
|
||||
|
||||
if not self._speed == SPEED_OFF:
|
||||
self._last_on_speed = self._speed
|
||||
|
||||
await self.send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_oscillate(self, oscillating: bool) -> None:
|
||||
"""Set oscillation of the fan."""
|
||||
self._oscillating = oscillating
|
||||
|
||||
await self.send_command()
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_direction(self, direction: str):
|
||||
"""Set the direction of the fan"""
|
||||
self._direction = direction
|
||||
|
||||
if not self._speed.lower() == SPEED_OFF:
|
||||
await self.send_command()
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, percentage: int = None, preset_mode: str = None, **kwargs):
|
||||
"""Turn on the fan."""
|
||||
if percentage is None:
|
||||
percentage = ordered_list_item_to_percentage(
|
||||
self._speed_list, self._last_on_speed or self._speed_list[0])
|
||||
|
||||
await self.async_set_percentage(percentage)
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off the fan."""
|
||||
await self.async_set_percentage(0)
|
||||
|
||||
async def send_command(self):
|
||||
async with self._temp_lock:
|
||||
self._on_by_remote = False
|
||||
speed = self._speed
|
||||
direction = self._direction or 'default'
|
||||
oscillating = self._oscillating
|
||||
|
||||
if speed.lower() == SPEED_OFF:
|
||||
command = self._commands['off']
|
||||
elif oscillating:
|
||||
command = self._commands['oscillate']
|
||||
else:
|
||||
command = self._commands[direction][speed]
|
||||
|
||||
try:
|
||||
await self._controller.send(command)
|
||||
except Exception as e:
|
||||
_LOGGER.exception(e)
|
||||
|
||||
@callback
|
||||
async def _async_power_sensor_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle power sensor changes."""
|
||||
entity_id = event.data["entity_id"]
|
||||
old_state = event.data["old_state"]
|
||||
new_state = event.data["new_state"]
|
||||
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if new_state.state == old_state.state:
|
||||
return
|
||||
|
||||
if new_state.state == STATE_ON and self._speed == SPEED_OFF:
|
||||
self._on_by_remote = True
|
||||
self._speed = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
if new_state.state == STATE_OFF:
|
||||
self._on_by_remote = False
|
||||
if self._speed != SPEED_OFF:
|
||||
self._speed = SPEED_OFF
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,386 @@
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import json
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
PLATFORM_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
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 = "SmartIR Light"
|
||||
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"
|
||||
|
||||
CMD_BRIGHTNESS_INCREASE = "brighten"
|
||||
CMD_BRIGHTNESS_DECREASE = "dim"
|
||||
CMD_COLORMODE_COLDER = "colder"
|
||||
CMD_COLORMODE_WARMER = "warmer"
|
||||
CMD_POWER_ON = "on"
|
||||
CMD_POWER_OFF = "off"
|
||||
CMD_NIGHTLIGHT = "night"
|
||||
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass,
|
||||
config,
|
||||
async_add_entities,
|
||||
discovery_info=None,
|
||||
):
|
||||
"""Set up the IR Light platform."""
|
||||
device_code = config.get(CONF_DEVICE_CODE)
|
||||
device_files_subdir = os.path.join("codes", "light")
|
||||
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/"
|
||||
"smartHomeHub/SmartIR/master/"
|
||||
"codes/light/{}.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([SmartIRLight(hass, config, device_data)])
|
||||
|
||||
|
||||
# find the closest match in a sorted list
|
||||
def closest_match(value, list):
|
||||
prev_val = None
|
||||
for index, entry in enumerate(list):
|
||||
if entry > (value or 0):
|
||||
if prev_val is None:
|
||||
return index
|
||||
diff_lo = value - prev_val
|
||||
diff_hi = entry - value
|
||||
if diff_lo < diff_hi:
|
||||
return index - 1
|
||||
return index
|
||||
prev_val = entry
|
||||
|
||||
return len(list) - 1
|
||||
|
||||
|
||||
class SmartIRLight(LightEntity, 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._brightnesses = device_data["brightness"]
|
||||
self._colortemps = device_data["colorTemperature"]
|
||||
self._commands = device_data["commands"]
|
||||
|
||||
self._power = STATE_ON
|
||||
self._brightness = None
|
||||
self._colortemp = None
|
||||
|
||||
self._temp_lock = asyncio.Lock()
|
||||
self._on_by_remote = False
|
||||
self._support_color_mode = ColorMode.UNKNOWN
|
||||
|
||||
if (
|
||||
CMD_COLORMODE_COLDER in self._commands
|
||||
and CMD_COLORMODE_WARMER in self._commands
|
||||
):
|
||||
self._colortemp = self.max_color_temp_kelvin
|
||||
self._support_color_mode = ColorMode.COLOR_TEMP
|
||||
|
||||
if CMD_NIGHTLIGHT in self._commands or (
|
||||
CMD_BRIGHTNESS_INCREASE in self._commands
|
||||
and CMD_BRIGHTNESS_DECREASE in self._commands
|
||||
):
|
||||
self._brightness = 100
|
||||
self._support_brightness = True
|
||||
if self._support_color_mode == ColorMode.UNKNOWN:
|
||||
self._support_color_mode = ColorMode.BRIGHTNESS
|
||||
else:
|
||||
self._support_brightness = False
|
||||
|
||||
if (
|
||||
CMD_POWER_OFF in self._commands
|
||||
and CMD_POWER_ON in self._commands
|
||||
and self._support_color_mode == ColorMode.UNKNOWN
|
||||
):
|
||||
self._support_color_mode = ColorMode.ONOFF
|
||||
|
||||
# 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._power = last_state.state
|
||||
if ATTR_BRIGHTNESS in last_state.attributes:
|
||||
self._brightness = last_state.attributes[ATTR_BRIGHTNESS]
|
||||
if ATTR_COLOR_TEMP_KELVIN in last_state.attributes:
|
||||
self._colortemp = last_state.attributes[ATTR_COLOR_TEMP_KELVIN]
|
||||
|
||||
if self._power_sensor:
|
||||
async_track_state_change_event(
|
||||
self.hass, self._power_sensor, self._async_power_sensor_changed
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of the light."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def supported_color_modes(self):
|
||||
"""Return the list of supported color modes."""
|
||||
return [self._support_color_mode]
|
||||
|
||||
@property
|
||||
def color_mode(self):
|
||||
return self._support_color_mode
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self):
|
||||
return self._colortemp
|
||||
|
||||
@property
|
||||
def min_color_temp_kelvin(self):
|
||||
if self._colortemps:
|
||||
return self._colortemps[0]
|
||||
|
||||
@property
|
||||
def max_color_temp_kelvin(self):
|
||||
if self._colortemps:
|
||||
return self._colortemps[-1]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self._power == STATE_ON or self._on_by_remote
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self._brightness
|
||||
|
||||
@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,
|
||||
"on_by_remote": self._on_by_remote,
|
||||
}
|
||||
|
||||
async def async_turn_on(self, **params):
|
||||
did_something = False
|
||||
# Turn the light on if off
|
||||
if self._power != STATE_ON and not self._on_by_remote:
|
||||
self._power = STATE_ON
|
||||
did_something = True
|
||||
await self.send_command(CMD_POWER_ON)
|
||||
|
||||
if (
|
||||
ATTR_COLOR_TEMP_KELVIN in params
|
||||
and ColorMode.COLOR_TEMP == self._support_color_mode
|
||||
):
|
||||
target = params.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
old_color_temp = closest_match(self._colortemp, self._colortemps)
|
||||
new_color_temp = closest_match(target, self._colortemps)
|
||||
_LOGGER.debug(
|
||||
f"Changing color temp from {self._colortemp}K step {old_color_temp} to {target}K step {new_color_temp}"
|
||||
)
|
||||
|
||||
steps = new_color_temp - old_color_temp
|
||||
did_something = True
|
||||
if steps < 0:
|
||||
cmd = CMD_COLORMODE_WARMER
|
||||
steps = abs(steps)
|
||||
else:
|
||||
cmd = CMD_COLORMODE_COLDER
|
||||
|
||||
if steps > 0 and cmd:
|
||||
# If we are heading for the highest or lowest value,
|
||||
# take the opportunity to resync by issuing enough
|
||||
# commands to go the full range.
|
||||
if new_color_temp == len(self._colortemps) - 1 or new_color_temp == 0:
|
||||
steps = len(self._colortemps)
|
||||
self._colortemp = self._colortemps[new_color_temp]
|
||||
await self.send_command(cmd, steps)
|
||||
|
||||
if ATTR_BRIGHTNESS in params and self._support_brightness:
|
||||
# before checking the supported brightnesses, make a special case
|
||||
# when a nightlight is fitted for brightness of 1
|
||||
if params.get(ATTR_BRIGHTNESS) == 1 and CMD_NIGHTLIGHT in self._commands:
|
||||
self._brightness = 1
|
||||
self._power = STATE_ON
|
||||
did_something = True
|
||||
await self.send_command(CMD_NIGHTLIGHT)
|
||||
|
||||
elif self._brightnesses:
|
||||
target = params.get(ATTR_BRIGHTNESS)
|
||||
old_brightness = closest_match(self._brightness, self._brightnesses)
|
||||
new_brightness = closest_match(target, self._brightnesses)
|
||||
did_something = True
|
||||
_LOGGER.debug(
|
||||
f"Changing brightness from {self._brightness} step {old_brightness} to {target} step {new_brightness}"
|
||||
)
|
||||
steps = new_brightness - old_brightness
|
||||
if steps < 0:
|
||||
cmd = CMD_BRIGHTNESS_DECREASE
|
||||
steps = abs(steps)
|
||||
else:
|
||||
cmd = CMD_BRIGHTNESS_INCREASE
|
||||
|
||||
if steps > 0 and cmd:
|
||||
# If we are heading for the highest or lowest value,
|
||||
# take the opportunity to resync by issuing enough
|
||||
# commands to go the full range.
|
||||
if (
|
||||
new_brightness == len(self._brightnesses) - 1
|
||||
or new_brightness == 0
|
||||
):
|
||||
steps = len(self._colortemps)
|
||||
did_something = True
|
||||
self._brightness = self._brightnesses[new_brightness]
|
||||
await self.send_command(cmd, steps)
|
||||
|
||||
# If we did nothing above, and the light is not detected as on
|
||||
# already issue the on command, even though we think the light
|
||||
# is on. This is because we may be out of sync due to use of the
|
||||
# remote when we don't have anything to detect it.
|
||||
# If we do have such monitoring, avoid issuing the command in case
|
||||
# on and off are the same remote code.
|
||||
if not did_something and not self._on_by_remote:
|
||||
self._power = STATE_ON
|
||||
await self.send_command(CMD_POWER_ON)
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self):
|
||||
self._power = STATE_OFF
|
||||
await self.send_command(CMD_POWER_OFF)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_toggle(self):
|
||||
await (self.async_turn_on() if not self.is_on else self.async_turn_off())
|
||||
|
||||
async def send_command(self, cmd, count=1):
|
||||
if cmd not in self._commands:
|
||||
_LOGGER.error(f"Unknown command '{cmd}'")
|
||||
return
|
||||
_LOGGER.debug(f"Sending {cmd} remote command {count} times.")
|
||||
remote_cmd = self._commands.get(cmd)
|
||||
async with self._temp_lock:
|
||||
self._on_by_remote = False
|
||||
try:
|
||||
for _ in range(count):
|
||||
await self._controller.send(remote_cmd)
|
||||
except Exception as e:
|
||||
_LOGGER.exception(e)
|
||||
|
||||
@callback
|
||||
async def _async_power_sensor_changed(self, event):
|
||||
"""Handle power sensor changes."""
|
||||
new_state = event.data["new_state"]
|
||||
if new_state is None:
|
||||
return
|
||||
old_state = event.data["old_state"]
|
||||
if new_state.state == old_state.state:
|
||||
return
|
||||
|
||||
if new_state.state == STATE_ON:
|
||||
self._on_by_remote = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
if new_state.state == STATE_OFF:
|
||||
self._on_by_remote = False
|
||||
self._power = STATE_OFF
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"domain": "smartir",
|
||||
"name": "SmartIR",
|
||||
"documentation": "https://github.com/smartHomeHub/SmartIR",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@smartHomeHub"],
|
||||
"requirements": ["aiofiles>=0.6.0"],
|
||||
"homeassistant": "2025.5.0",
|
||||
"version": "1.18.1",
|
||||
"updater": {
|
||||
"version": "1.18.1",
|
||||
"releaseNotes": "-- Implements new async_track_state_change_event",
|
||||
"files": [
|
||||
"__init__.py",
|
||||
"climate.py",
|
||||
"media_player.py",
|
||||
"fan.py",
|
||||
"light.py",
|
||||
"controller.py",
|
||||
"manifest.json",
|
||||
"services.yaml"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
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 = "SmartIR 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_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/"
|
||||
"smartHomeHub/SmartIR/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([SmartIRMediaPlayer(
|
||||
hass, config, device_data
|
||||
)])
|
||||
|
||||
class SmartIRMediaPlayer(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
|
||||
@@ -0,0 +1,4 @@
|
||||
check_updates:
|
||||
description: Check for SmartIR updates.
|
||||
update_component:
|
||||
description: Update SmartIR component.
|
||||
Reference in New Issue
Block a user