import os import re import json import locale import aiohttp import asyncio import tzlocal import logging import fnmatch import voluptuous as vol from typing import Type, Tuple, Optional, Callable, Set from functools import wraps from homeassistant.core import HomeAssistant, split_entity_id # noqa from homeassistant.util import slugify, language as language_util from homeassistant.util.dt import DEFAULT_TIME_ZONE, get_time_zone from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import DOMAIN, DEVICE_CUSTOMIZES, DATA_CUSTOMIZE from .translation_languages import TRANSLATION_LANGUAGES def get_value(obj, key, def_value=None, sep='.'): keys = f'{key}'.split(sep) result = obj for k in keys: if result is None: return None if isinstance(result, dict): result = result.get(k, def_value) elif isinstance(result, (list, tuple)): try: result = result[int(k)] except Exception: result = def_value return result def slugify_object_id(value): if obj := slugify(f'{value}'): return obj obj = re.sub(r'\W+', '_', f'{value}').lower().strip('_') return obj or 'miot' def get_customize_via_model(model, key=None, default=None): cfg = {} for m in wildcard_models(model): cus = DEVICE_CUSTOMIZES.get(m) or {} if key is not None and key not in cus: continue if cus: cfg = {**cus, **cfg} return cfg if key is None else cfg.get(key, default) def get_customize_via_entity(entity, key=None, default=None): if key is None: default = {} if not isinstance(entity, Entity): return default cfg = {} if entity.hass and entity.entity_id: cfg = { **(entity.hass.data[DATA_CUSTOMIZE].get(entity.entity_id) or {}), **(entity.hass.data[DOMAIN].get(DATA_CUSTOMIZE, {}).get(entity.entity_id) or {}), } if key is not None and key in cfg: return cfg.get(key) mls = [] if model := getattr(entity, 'model', None): if hasattr(entity, 'customize_keys'): mls.extend(entity.customize_keys) mls.append(model) for mod in mls: cus = get_customize_via_model(mod) cfg = {**cus, **cfg} return cfg if key is None else cfg.get(key, default) class CustomConfigHelper: def custom_config(self, key=None, default=None): raise NotImplementedError def custom_config_bool(self, key=None, default=None): val = self.custom_config(key, default) try: val = cv.boolean(val) except vol.Invalid: val = default return val def custom_config_number(self, key=None, default=None): num = default val = self.custom_config(key) if val is not None: try: num = float(f'{val}') except (TypeError, ValueError): num = default return num def custom_config_integer(self, key=None, default=None): num = self.custom_config_number(key, default) if num is not None: num = int(num) return num def custom_config_list(self, key=None, default=None): lst = self.custom_config(key) if lst is None: return default if isinstance(lst, (int, float, bool)): lst = [lst] elif not isinstance(lst, list): lst = f'{lst}'.split(',') lst = list(map(lambda x: x.strip(), lst)) return lst def custom_config_json(self, key=None, default=None): dic = self.custom_config(key) if dic: if not isinstance(dic, (dict, list)): try: dic = json.loads(dic or '{}') except (TypeError, ValueError): dic = None if isinstance(dic, (dict, list)): return dic return default def get_manifest(field=None, default=None): manifest = {} with open(f'{os.path.dirname(__file__)}/../manifest.json') as fil: manifest = json.load(fil) or {} return manifest.get(field, default) if field else manifest async def async_get_manifest(hass: HomeAssistant, field=None, default=None): return await hass.async_add_executor_job(get_manifest, field, default) def local_zone(hass=None): try: if isinstance(hass, HomeAssistant): return get_time_zone(hass.config.time_zone) return tzlocal.get_localzone() except KeyError: pass return DEFAULT_TIME_ZONE def in_china(hass=None): if isinstance(hass, HomeAssistant): if hass.config.time_zone in ['Asia/Shanghai', 'Asia/Hong_Kong']: return True try: return f'{locale.getdefaultlocale()[0]}'[:3] == 'zh_' except (KeyError, Exception): pass return False def wildcard_models(model): if not model: return [] if ':' in model: return [model] wil = re.sub(r'\.[^.]+$', '.*', model) return [ model, wil, re.sub(r'^[^.]+\.', '*.', wil), '*', ] def convert_globs_to_pattern(globs: list[str] | None): """Convert a list of globs to a re pattern list.""" if not globs: return None translated_patterns = [ pattern for glob in set(globs) if (pattern := fnmatch.translate(str(glob))) ] if not translated_patterns: return None inner = '|'.join(translated_patterns) combined = f'(?:{inner})' return re.compile(combined) def get_translation(key, keys=None): dic = get_translations(*(keys or [])) val = dic.get(key, key) if isinstance(val, str): return val return key def get_translations(*keys): dic = { **TRANSLATION_LANGUAGES, **(TRANSLATION_LANGUAGES.get('_globals', {})), } for k in keys: tls = TRANSLATION_LANGUAGES.get(k) if not isinstance(tls, dict): continue dic.update(tls) return dic def get_translation_langs(hass: HomeAssistant, langs=None): lang = hass.config.language if not langs: return [lang] if 'en' not in langs: langs.append('en') return language_util.matches(lang, langs) class DeviceException(Exception): """Exception wrapping any communication errors with the device.""" def is_offline_exception(exc): err = f'{exc}' ret = 'Unable to discover the device' in err if not ret: ret = 'No response from the device' in err if not ret: ret = 'OSError: [Errno 64] Host is down' in err if not ret: ret = 'OSError: [Errno 65] No route to host' in err return ret def update_attrs_with_suffix(attrs, new_dict): updated_attrs = {} for key, value in new_dict.items(): if key in attrs: suffix = 2 while f"{key}_{suffix}" in attrs: suffix += 1 updated_key = f"{key}_{suffix}" else: updated_key = key updated_attrs[updated_key] = value attrs.update(updated_attrs) def logger_filter(record): def sub(msg): return re.sub(r'[\w/+-]{30,}', '***', msg) if isinstance(record, str): return sub(record) if isinstance(record, logging.LogRecord): record.msg = sub(record.msg) return True return record def int_to_rgb(x: int) -> Tuple[int, int, int]: """Return a RGB tuple from integer.""" red = (x >> 16) & 0xFF green = (x >> 8) & 0xFF blue = x & 0xFF return red, green, blue def rgb_to_int(x: Tuple[int, int, int]) -> int: """Return an integer from RGB tuple.""" return int(x[0] << 16 | x[1] << 8 | x[2]) def aiohttp_retry( max_retries: int = 3, backoff_factor: float = 2.0, retry_on_status: Optional[Set[int]] = None, exceptions: Tuple[Type[BaseException], ...] = (aiohttp.ClientError, asyncio.TimeoutError), logger=None, ): if retry_on_status is None: retry_on_status = {500, 502, 503, 504} def decorator(func: Callable): @wraps(func) async def wrapper(*args, **kwargs): last_exception = None for attempt in range(max_retries + 1): try: return await func(*args, **kwargs) except exceptions as exc: if logger: logger.warning( "Error while executing request from %s, attempt: %s", func.__name__, attempt, exc_info=True, ) last_exception = exc if attempt == max_retries: break is_retryable_status = isinstance(exc, aiohttp.ClientConnectorDNSError) if isinstance(exc, aiohttp.ClientResponseError) and exc.status in retry_on_status: is_retryable_status = True if not is_retryable_status: break delay = backoff_factor * (2 ** attempt) await asyncio.sleep(delay) if last_exception: raise last_exception return wrapper return decorator async def async_analytics_track_event(hass: HomeAssistant, event, action, label, value=0, **kwargs): pms = { 'model': label, 'event': event, 'action': action, 'label': label, 'value': value, 'tz': hass.config.time_zone, 'ver': await async_get_manifest(hass, 'version', ''), **kwargs, } url = 'https://hacc.miot-spec.com/api/track' try: session = async_get_clientsession(hass) return await session.post(url, data=pms, timeout=3) except (Exception, ValueError): return False class RC4: _idx = 0 _jdx = 0 _ksa: list def __init__(self, pwd): self.init_key(pwd) def init_key(self, pwd): cnt = len(pwd) ksa = list(range(256)) j = 0 for i in range(256): j = (j + ksa[i] + pwd[i % cnt]) & 255 ksa[i], ksa[j] = ksa[j], ksa[i] self._ksa = ksa self._idx = 0 self._jdx = 0 return self def crypt(self, data): if isinstance(data, str): data = data.encode() ksa = self._ksa i = self._idx j = self._jdx out = [] for byt in data: i = (i + 1) & 255 j = (j + ksa[i]) & 255 ksa[i], ksa[j] = ksa[j], ksa[i] out.append(byt ^ ksa[(ksa[i] + ksa[j]) & 255]) self._idx = i self._jdx = j self._ksa = ksa return bytearray(out) def init1024(self): self.crypt(bytes(1024)) return self