992098fbf1
- Implemented a new config flow in `config_flow.py` for SmartIR, allowing users to select platforms, controllers, and devices through a guided interface. - Added support for new device codes in `codes_index.json` for Apton and Andersson manufacturers. - Enhanced fan, light, and media player components to support setup from config entries. - Introduced `strings.json` and `translations/en.json` for improved localization of the configuration steps. - Created a tool `build_index.py` to rebuild the `codes_index.json` from individual device JSON files. - Updated `smartir_capture.py` to handle single-code modes and excluded modes for better device configuration. - Added `iot_class` and `config_flow` attributes in `manifest.json` to support the new configuration flow.
316 lines
11 KiB
Python
316 lines
11 KiB
Python
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_entry(hass, entry, async_add_entities):
|
|
"""Set up SmartIR Fan 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 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() |