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.
196 lines
7.4 KiB
Python
196 lines
7.4 KiB
Python
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 async_setup_entry(hass, entry):
|
|
"""Set up SmartIR from a config entry (UI-configured device)."""
|
|
platform = entry.data.get('platform')
|
|
if not platform:
|
|
return False
|
|
await hass.config_entries.async_forward_entry_setups(entry, [platform])
|
|
# Reload the entry whenever options are changed in the UI so the new
|
|
# controller_data / delay / sensor settings take effect immediately.
|
|
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
|
|
return True
|
|
|
|
|
|
async def _async_options_updated(hass, entry):
|
|
"""Triggered when the options flow saves; reloads the config entry."""
|
|
await hass.config_entries.async_reload(entry.entry_id)
|
|
|
|
|
|
async def async_unload_entry(hass, entry):
|
|
"""Unload a SmartIR config entry."""
|
|
platform = entry.data.get('platform')
|
|
if not platform:
|
|
return True
|
|
return await hass.config_entries.async_unload_platforms(entry, [platform])
|
|
|
|
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 |