463 lines
17 KiB
Python
463 lines
17 KiB
Python
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 = "Infrarize 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_entry(hass, entry, async_add_entities):
|
|
"""Set up Infrarize Climate 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 Climate platform."""
|
|
_LOGGER.debug("Setting up the infrarize 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/"
|
|
"kobimx/Infrarize/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([InfrarizeClimate(
|
|
hass, config, device_data
|
|
)])
|
|
|
|
class InfrarizeClimate(ClimateEntity, RestoreEntity):
|
|
def __init__(self, hass, config, device_data):
|
|
_LOGGER.debug(f"InfrarizeClimate 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) |