diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d0c8215 --- /dev/null +++ b/__init__.py @@ -0,0 +1,311 @@ +"""The Terncy integration.""" +import asyncio +import logging + +import terncy +import terncy.event +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity_platform import async_get_platforms + +from .const import ( + DOMAIN, + HA_CLIENT_ID, + PROFILE_COLOR_DIMMABLE_LIGHT, + PROFILE_COLOR_LIGHT, + PROFILE_COLOR_TEMPERATURE_LIGHT, + PROFILE_DIMMABLE_COLOR_TEMPERATURE_LIGHT, + PROFILE_DIMMABLE_LIGHT, + PROFILE_DIMMABLE_LIGHT2, + PROFILE_EXTENDED_COLOR_LIGHT, + PROFILE_EXTENDED_COLOR_LIGHT2, + PROFILE_ONOFF_LIGHT, + TERNCY_EVENT_SVC_ADD, + TERNCY_EVENT_SVC_REMOVE, + TERNCY_HUB_ID_PREFIX, + TERNCY_MANU_NAME, + TerncyHassPlatformData, +) +from .hub_monitor import TerncyHubManager +from .light import ( + SUPPORT_TERNCY_COLOR, + SUPPORT_TERNCY_CT, + SUPPORT_TERNCY_DIMMABLE, + SUPPORT_TERNCY_EXTENDED, + SUPPORT_TERNCY_ON_OFF, + TerncyLight, +) + +PLATFORM_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +PLATFORMS = ["light"] + +_LOGGER = logging.getLogger(__name__) + + +def find_dev_by_prefix(devices, prefix): + """Find device with given prefix.""" + result = [] + for dev in devices.values(): + if dev.unique_id.startswith(prefix): + result.append(dev) + return result + + +def terncy_event_handler(tern, ev): + """Handle event from terncy system.""" + hass = tern.hass_platform_data.hass + parsed_devices = tern.hass_platform_data.parsed_devices + if isinstance(ev, terncy.event.Connected): + _LOGGER.info("got connected event %s", tern.dev_id) + asyncio.ensure_future(async_refresh_devices(hass, tern)) + if isinstance(ev, terncy.event.Disconnected): + _LOGGER.info("got disconnected event %s", tern.dev_id) + for dev in parsed_devices.values(): + dev.is_available = False + dev.schedule_update_ha_state() + if isinstance(ev, terncy.event.EventMessage): + _LOGGER.info("got event message %s %s", tern.dev_id, ev.msg) + evt_type = "" + if "type" in ev.msg: + evt_type = ev.msg["type"] + if "entities" not in ev.msg: + return + ents = ev.msg["entities"] + if evt_type == "report": + for ent in ents: + if "attributes" not in ent: + continue + devid = ent["id"] + + if devid in parsed_devices: + dev = parsed_devices[devid] + attrs = ent["attributes"] + dev.update_state(attrs) + dev.schedule_update_ha_state() + elif evt_type == "entityAvailable": + for ent in ents: + devid = ent["id"] + _LOGGER.info("[%s] %s is available", tern.dev_id, devid) + hass.async_create_task(update_or_create_entity(ent, tern)) + elif evt_type == "offline": + for ent in ents: + devid = ent["id"] + _LOGGER.info("[%s] %s is offline", tern.dev_id, devid) + if devid in parsed_devices: + dev = parsed_devices[devid] + dev.is_available = False + dev.schedule_update_ha_state() + elif devid.rfind("-") > 0: + prefix = devid[0 : devid.rfind("-")] + _LOGGER.info( + "[%s] %s not found, try find prefix", tern.dev_id, prefix + ) + devs = find_dev_by_prefix(parsed_devices, prefix) + for dev in devs: + _LOGGER.info("[%s] %s is offline", tern.dev_id, dev.unique_id) + dev.is_available = False + dev.schedule_update_ha_state() + elif evt_type == "entityDeleted": + platform = None + for plat in async_get_platforms(hass, DOMAIN): + if plat.config_entry.unique_id == tern.dev_id: + if plat.domain == "light": + platform = plat + break + if platform is None: + return + for ent in ents: + devid = ent["id"] + _LOGGER.info("[%s] %s is deleted", tern.dev_id, devid) + if devid in parsed_devices: + dev = parsed_devices[devid] + dev.is_available = False + dev.schedule_update_ha_state() + elif devid.rfind("-") > 0: + prefix = devid[0 : devid.rfind("-")] + _LOGGER.info( + "[%s] %s not found, try find prefix", tern.dev_id, prefix + ) + devs = find_dev_by_prefix(parsed_devices, prefix) + for dev in devs: + _LOGGER.info("[%s] %s is delete", tern.dev_id, dev.unique_id) + hass.async_create_task( + platform.async_remove_entity(dev.entity_id) + ) + parsed_devices.pop(dev.unique_id) + else: + _LOGGER.info("unsupported event type %s", evt_type) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Terncy component.""" + return True + + +async def update_or_create_entity(dev, tern): + """Update or create hass entity for given terncy device.""" + model = dev["model"] if "model" in dev else "" + version = dev["version"] if "version" in dev else "" + available = dev["online"] if "online" in dev else False + if "services" not in dev: + return [] + for svc in dev["services"]: + profile = svc["profile"] + features = -1 + if profile == PROFILE_ONOFF_LIGHT: + features = SUPPORT_TERNCY_ON_OFF + elif profile == PROFILE_COLOR_LIGHT: + features = SUPPORT_TERNCY_COLOR + elif profile == PROFILE_EXTENDED_COLOR_LIGHT: + features = SUPPORT_TERNCY_EXTENDED + elif profile == PROFILE_COLOR_TEMPERATURE_LIGHT: + features = SUPPORT_TERNCY_CT + elif profile == PROFILE_DIMMABLE_COLOR_TEMPERATURE_LIGHT: + features = SUPPORT_TERNCY_CT + elif profile == PROFILE_DIMMABLE_LIGHT: + features = SUPPORT_TERNCY_DIMMABLE + elif profile == PROFILE_DIMMABLE_LIGHT2: + features = SUPPORT_TERNCY_DIMMABLE + elif profile == PROFILE_COLOR_DIMMABLE_LIGHT: + features = SUPPORT_TERNCY_EXTENDED + elif profile == PROFILE_EXTENDED_COLOR_LIGHT2: + features = SUPPORT_TERNCY_EXTENDED + else: + _LOGGER.info("unsupported profile %d", profile) + return [] + + devid = svc["id"] + name = svc["name"] + if name == "": + name = devid + light = None + if devid in tern.hass_platform_data.parsed_devices: + light = tern.hass_platform_data.parsed_devices[devid] + else: + light = TerncyLight(tern, devid, name, model, version, features) + light.update_state(svc["attributes"]) + light.is_available = available + if devid in tern.hass_platform_data.parsed_devices: + light.schedule_update_ha_state() + else: + platform = None + for platform in async_get_platforms(tern.hass_platform_data.hass, DOMAIN): + if platform.config_entry.unique_id == tern.dev_id: + if platform.domain == "light": + await platform.async_add_entities([light]) + tern.hass_platform_data.parsed_devices[devid] = light + + +async def async_refresh_devices(hass: HomeAssistant, tern): + """Get devices from terncy.""" + _LOGGER.info("refresh devices now") + response = await tern.get_entities("device", True) + devices = response["rsp"]["entities"] + pdata = tern.hass_platform_data + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=pdata.hub_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, pdata.mac)}, + identifiers={(DOMAIN, pdata.hub_entry.entry_id)}, + manufacturer=TERNCY_MANU_NAME, + name=pdata.hub_entry.title, + model="TERNCY-GW01", + sw_version=1, + ) + + for dev in devices: + await update_or_create_entity(dev, tern) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Terncy from a config entry.""" + _LOGGER.info("terncy domain async_setup_entry %s", entry.unique_id) + dev_id = entry.data["identifier"] + hass.data[DOMAIN] = {} + mgr = TerncyHubManager.instance(hass) + await mgr.start_discovery() + + tern = terncy.Terncy( + HA_CLIENT_ID, + dev_id, + entry.data["host"], + entry.data["port"], + entry.data["username"], + entry.data["token"], + ) + + pdata = TerncyHassPlatformData() + + pdata.hass = hass + pdata.hub_entry = entry + pdata.mac = dr.format_mac(entry.unique_id.replace(TERNCY_HUB_ID_PREFIX, "")) + tern.hass_platform_data = pdata + hass.data[DOMAIN][entry.entry_id] = tern + + async def setup_terncy_loop(): + asyncio.create_task(tern.start()) + + async def on_hass_stop(event): + """Stop push updates when hass stops.""" + _LOGGER.info("terncy domain stop") + await tern.stop() + + async def on_terncy_svc_add(event): + """Stop push updates when hass stops.""" + dev_id = event.data["dev_id"] + _LOGGER.info("found terncy service: %s %s", dev_id, event.data) + host = event.data["ip"] + if dev_id == tern.dev_id and not tern.is_connected(): + tern.host = host + _LOGGER.info("start connection to %s %s", dev_id, tern.host) + + hass.async_create_task(setup_terncy_loop()) + + async def on_terncy_svc_remove(event): + """Stop push updates when hass stops.""" + dev_id = event.data["dev_id"] + _LOGGER.info("terncy svc remove %s", dev_id) + if not tern.is_connected(): + await tern.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + hass.bus.async_listen(TERNCY_EVENT_SVC_ADD, on_terncy_svc_add) + hass.bus.async_listen(TERNCY_EVENT_SVC_REMOVE, on_terncy_svc_remove) + + manager = TerncyHubManager.instance(hass) + if dev_id in manager.hubs: + if not tern.is_connected(): + tern.host = manager.hubs[dev_id]["ip"] + _LOGGER.info("start connection to %s %s", dev_id, tern.host) + hass.async_create_task(setup_terncy_loop()) + + tern.register_event_handler(terncy_event_handler) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(pdata.hub_entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/config_flow.py b/config_flow.py new file mode 100644 index 0000000..3179bae --- /dev/null +++ b/config_flow.py @@ -0,0 +1,165 @@ +"""Config flow for Terncy integration.""" +import logging +import uuid + +import terncy +import voluptuous as vol + +from homeassistant import config_entries + +from .const import ( + CONF_DEVICE, + CONF_HOST, + CONF_IP, + CONF_NAME, + CONF_PORT, + DOMAIN, + TERNCY_HUB_SVC_NAME, +) +from .hub_monitor import TerncyHubManager + +_LOGGER = logging.getLogger(__name__) + + +async def _start_discovery(mgr): + await mgr.start_discovery() + + +def _get_discovered_devices(mgr): + return {} if mgr is None else mgr.hubs + + +def _get_terncy_instance(flow): + return flow.terncy + + +class TerncyConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Terncy.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the config flow.""" + self._discovered_devices = {} + + self.username = "ha_user_" + uuid.uuid4().hex[0:5] + self.client_id = "homeass_nbhQ43" + self.identifier = "" + self.name = "" + self.host = "" + self.port = 443 + self.token = "" + self.token_id = 0 + self.context = {} + self.terncy = terncy.Terncy( + self.client_id, + self.identifier, + self.host, + self.port, + self.username, + "VALID_TOKEN_NOT_ACQUIRED", + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + devices_name = {} + mgr = TerncyHubManager.instance(self.hass) + if user_input is not None and CONF_DEVICE in user_input: + devid = user_input[CONF_DEVICE] + hub = _get_discovered_devices(mgr)[devid] + self.identifier = devid + self.name = hub[CONF_NAME] + self.host = hub[CONF_IP] + self.port = hub[CONF_PORT] + _LOGGER.info("construct Terncy obj for %s %s", self.name, self.host) + self.terncy = terncy.Terncy( + self.client_id, self.identifier, self.host, self.port, self.username, "" + ) + return self.async_show_form( + step_id="begin_pairing", + description_placeholders={"name": self.name}, + ) + + for devid, hub in _get_discovered_devices(mgr).items(): + devices_name[devid] = hub[CONF_NAME] + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}), + ) + + async def async_step_begin_pairing(self, user_input=None): + """Start pairing process for the next available protocol.""" + if self.unique_id is None: + await self.async_set_unique_id(self.identifier) + self._abort_if_unique_id_configured() + ternobj = _get_terncy_instance(self) + if self.token == "": + _LOGGER.warning("request a new token form terncy %s", self.identifier) + code, token_id, token, state = await ternobj.request_token( + self.username, "HA User" + ) + self.token = token + self.token_id = token_id + self.terncy.token = token + ternobj = _get_terncy_instance(self) + code, state = await ternobj.check_token_state(self.token_id, self.token) + if code != 200: + _LOGGER.warning("current token invalid, clear it") + self.token = "" + self.token_id = 0 + errors = {} + errors["base"] = "need_new_auth" + return self.async_show_form( + step_id="begin_pairing", + description_placeholders={"name": self.name}, + errors=errors, + ) + if state == terncy.TokenState.APPROVED.value: + _LOGGER.warning("token valid, create entry for %s", self.identifier) + return self.async_create_entry( + title=self.name, + data={ + "identifier": self.identifier, + "username": self.username, + "token": self.token, + "token_id": self.token_id, + "host": self.host, + "port": self.port, + }, + ) + errors = {} + errors["base"] = "invalid_auth" + return self.async_show_form( + step_id="begin_pairing", + description_placeholders={"name": self.name}, + errors=errors, + ) + + async def async_step_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self.async_step_begin_pairing() + return self.async_show_form( + step_id="confirm", description_placeholders={"name": self.name} + ) + + async def async_step_zeroconf(self, discovery_info): + """Prepare configuration for a discovered Daikin device.""" + identifier = discovery_info["name"] + identifier = identifier.replace("." + TERNCY_HUB_SVC_NAME, "") + properties = discovery_info["properties"] + name = properties[CONF_NAME] + self.context["identifier"] = self.unique_id + self.context["title_placeholders"] = {"name": name} + self.identifier = identifier + self.name = name + self.host = discovery_info[CONF_HOST] + self.port = discovery_info[CONF_PORT] + self.terncy.ip = self.host + self.terncy.port = self.port + mgr = TerncyHubManager.instance(self.hass) + _LOGGER.info("start discovery engine of domain %s", DOMAIN) + await _start_discovery(mgr) + return await self.async_step_confirm() diff --git a/const.py b/const.py new file mode 100644 index 0000000..27979b3 --- /dev/null +++ b/const.py @@ -0,0 +1,41 @@ +"""Constants for the Terncy integration.""" + +DOMAIN = "terncy" +HA_CLIENT_ID = "homeass_nbhQ43" + +TERNCY_HUB_ID_PREFIX = "box-" +TERNCY_HUB_SVC_NAME = "_websocket._tcp.local." +TERNCY_MANU_NAME = "Xiaoyan Tech." + +TERNCY_EVENT_SVC_ADD = "terncy_svc_add" +TERNCY_EVENT_SVC_REMOVE = "terncy_svc_remove" +TERNCY_EVENT_SVC_UPDATE = "terncy_svc_update" + +PROFILE_COLOR_DIMMABLE_LIGHT = 26 +PROFILE_COLOR_LIGHT = 8 +PROFILE_COLOR_TEMPERATURE_LIGHT = 13 +PROFILE_DIMMABLE_COLOR_TEMPERATURE_LIGHT = 17 +PROFILE_DIMMABLE_LIGHT = 19 +PROFILE_DIMMABLE_LIGHT2 = 20 +PROFILE_EXTENDED_COLOR_LIGHT = 12 +PROFILE_EXTENDED_COLOR_LIGHT2 = 27 +PROFILE_ONOFF_LIGHT = 2 + +CONF_DEVID = "dev_id" +CONF_DEVICE = "device" +CONF_NAME = "dn" +CONF_HOST = "host" +CONF_IP = "ip" +CONF_PORT = "port" + + +class TerncyHassPlatformData: + """Hold HASS platform data for Terncy component.""" + + def __init__(self): + """Create platform data.""" + self.mac = "" + self.hass = None + self.hub_entry = None + self.initialized = False + self.parsed_devices = {} diff --git a/hub_monitor.py b/hub_monitor.py new file mode 100644 index 0000000..0ccf3ec --- /dev/null +++ b/hub_monitor.py @@ -0,0 +1,110 @@ +"""Constants for the Terncy integration.""" + +import ipaddress +import logging + +from zeroconf import ServiceBrowser + +from homeassistant.components import zeroconf as hasszeroconf + +from .const import ( + CONF_DEVID, + CONF_IP, + CONF_PORT, + TERNCY_EVENT_SVC_ADD, + TERNCY_EVENT_SVC_REMOVE, + TERNCY_EVENT_SVC_UPDATE, + TERNCY_HUB_SVC_NAME, +) + +_LOGGER = logging.getLogger(__name__) + + +def _parse_svc(dev_id, info): + txt_records = {CONF_DEVID: dev_id} + ip_addr = "" + if len(info.addresses) > 0: + if len(info.addresses[0]) == 4: + ip_addr = str(ipaddress.IPv4Address(info.addresses[0])) + if len(info.addresses[0]) == 16: + ip_addr = str(ipaddress.IPv6Address(info.addresses[0])) + txt_records[CONF_IP] = ip_addr + txt_records[CONF_PORT] = info.port + for k in info.properties: + txt_records[k.decode("utf-8")] = info.properties[k].decode("utf-8") + return txt_records + + +class TerncyZCListener: + """Terncy zeroconf discovery listener.""" + + def __init__(self, manager): + """Create Terncy discovery listener.""" + self.manager = manager + + def remove_service(self, zconf, svc_type, name): + """Get a terncy service removed event.""" + dev_id = name.replace("." + svc_type, "") + if dev_id in self.manager.hubs: + del self.manager.hubs[dev_id] + txt_records = {CONF_DEVID: dev_id} + self.manager.hass.bus.async_fire(TERNCY_EVENT_SVC_REMOVE, txt_records) + + def update_service(self, zconf, svc_type, name): + """Get a terncy service updated event.""" + info = zconf.get_service_info(svc_type, name) + if info is None: + return + dev_id = name.replace("." + svc_type, "") + txt_records = _parse_svc(dev_id, info) + + self.manager.hubs[dev_id] = txt_records + self.manager.hass.bus.async_fire(TERNCY_EVENT_SVC_UPDATE, txt_records) + + def add_service(self, zconf, svc_type, name): + """Get a new terncy service discovered event.""" + info = zconf.get_service_info(svc_type, name) + if info is None: + return + dev_id = name.replace("." + svc_type, "") + txt_records = _parse_svc(dev_id, info) + + self.manager.hubs[dev_id] = txt_records + self.manager.hass.bus.async_fire(TERNCY_EVENT_SVC_ADD, txt_records) + + +class TerncyHubManager: + """Manager of terncy hubs.""" + + __instance = None + + def __init__(self, hass): + """Create instance of terncy manager, use instance instead.""" + self.hass = hass + self._browser = None + self._discovery_engine = None + self.hubs = {} + TerncyHubManager.__instance = self + + @staticmethod + def instance(hass): + """Get singleton instance of terncy manager.""" + if TerncyHubManager.__instance is None: + TerncyHubManager(hass) + return TerncyHubManager.__instance + + async def start_discovery(self): + """Start terncy discovery engine.""" + if not self._discovery_engine: + zconf = await hasszeroconf.async_get_instance(self.hass) + self._discovery_engine = zconf + listener = TerncyZCListener(self) + self._browser = ServiceBrowser(zconf, TERNCY_HUB_SVC_NAME, listener) + + async def stop_discovery(self): + """Stop terncy discovery engine.""" + if self._discovery_engine: + self._browser.cancel() + self._discovery_engine.close() + self._browser = None + self._discovery_engine = None diff --git a/light.py b/light.py new file mode 100644 index 0000000..c2e9f75 --- /dev/null +++ b/light.py @@ -0,0 +1,192 @@ +"""Light platform support for Terncy.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_FLASH, + LightEntity, +) + +from .const import DOMAIN, TERNCY_MANU_NAME + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_TERNCY_ON_OFF = SUPPORT_FLASH +SUPPORT_TERNCY_DIMMABLE = SUPPORT_TERNCY_ON_OFF | SUPPORT_BRIGHTNESS +SUPPORT_TERNCY_CT = SUPPORT_TERNCY_DIMMABLE | SUPPORT_COLOR_TEMP +SUPPORT_TERNCY_COLOR = SUPPORT_TERNCY_DIMMABLE | SUPPORT_COLOR +SUPPORT_TERNCY_EXTENDED = SUPPORT_TERNCY_DIMMABLE | SUPPORT_COLOR | SUPPORT_COLOR_TEMP + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up Terncy lights. + + Can only be called when a user accidentally mentions Terncy platform in their + config. But even in that case it would have been ignored. + """ + _LOGGER.info(" terncy light async_setup_platform") + + +def get_attr_value(attrs, key): + """Read attr value from terncy attributes.""" + for att in attrs: + if "attr" in att and att["attr"] == key: + return att["value"] + return None + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Terncy lights from a config entry.""" + _LOGGER.info("setup terncy light platform") + + +class TerncyLight(LightEntity): + """Representation of a Terncy light.""" + + def __init__(self, api, devid, name, model, version, features): + """Initialize the light.""" + self._device_id = devid + self.hub_id = api.dev_id + self._name = name + self.model = model + self.version = version + self.api = api + self.is_available = False + self._features = features + self._onoff = False + self._ct = 0 + self._hs = (0, 0) + self._bri = 0 + + def update_state(self, attrs): + """Updateterncy state.""" + _LOGGER.info("update state event to %s", attrs) + on_off = get_attr_value(attrs, "on") + if on_off is not None: + self._onoff = on_off == 1 + bri = get_attr_value(attrs, "brightness") + if bri: + self._bri = int(bri / 100 * 255) + color_temp = get_attr_value(attrs, "colorTemperature") + if color_temp is not None: + self._ct = color_temp + hue = get_attr_value(attrs, "hue") + sat = get_attr_value(attrs, "saturation") + if hue is not None: + hue = hue / 255 * 360.0 + self._hs = (hue, self._hs[1]) + if sat is not None: + sat = sat / 255 * 100 + self._hs = (self._hs[0], sat) + + @property + def unique_id(self): + """Return terncy unique id.""" + return self._device_id + + @property + def device_id(self): + """Return terncy device id.""" + return self._device_id + + @property + def name(self): + """Return terncy device name.""" + return self._name + + @property + def brightness(self): + """Return terncy device brightness.""" + return self._bri + + @property + def hs_color(self): + """Return terncy device color.""" + return self._hs + + @property + def color_temp(self): + """Return terncy device color temperature.""" + return self._ct + + @property + def min_mireds(self): + """Return terncy device min mireds.""" + return 50 + + @property + def max_mireds(self): + """Return terncy device max mireds.""" + return 400 + + @property + def is_on(self): + """Return if terncy device is on.""" + return self._onoff + + @property + def available(self): + """Return if terncy device is available.""" + return self.is_available + + @property + def supported_features(self): + """Return the terncy device feature.""" + return self._features + + @property + def device_info(self): + """Return the terncy device info.""" + return { + "identifiers": {(DOMAIN, self.device_id)}, + "name": self.name, + "manufacturer": TERNCY_MANU_NAME, + "model": self.model, + "sw_version": self.version, + "via_device": (DOMAIN, self.hub_id), + } + + async def async_turn_on(self, **kwargs): + """Turn on terncy light.""" + _LOGGER.info("turn on %s", kwargs) + await self.api.set_onoff(self._device_id, 1) + self._onoff = True + if ATTR_BRIGHTNESS in kwargs: + bri = kwargs.get(ATTR_BRIGHTNESS) + terncy_bri = int(bri / 255 * 100) + await self.api.set_attribute(self._device_id, "brightness", terncy_bri, 0) + self._bri = bri + if ATTR_COLOR_TEMP in kwargs: + color_temp = kwargs.get(ATTR_COLOR_TEMP) + if color_temp < 50: + color_temp = 50 + if color_temp > 400: + color_temp = 400 + await self.api.set_attribute( + self._device_id, "colorTemperature", color_temp, 0 + ) + self._ct = color_temp + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs.get(ATTR_HS_COLOR) + terncy_hue = int(hs_color[0] / 360 * 255) + terncy_sat = int(hs_color[1] / 100 * 255) + await self.api.set_attribute(self._device_id, "hue", terncy_hue, 0) + await self.api.set_attribute(self._device_id, "sat", terncy_sat, 0) + self._hs = hs_color + + async def async_turn_off(self, **kwargs): + """Turn off terncy light.""" + _LOGGER.info("turn off") + self._onoff = False + await self.api.set_onoff(self._device_id, 0) + self.async_write_ha_state() + + @property + def device_state_attributes(self): + """Get terncy light states.""" + return {} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..a72a6e4 --- /dev/null +++ b/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "terncy", + "name": "Terncy", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/terncy", + "requirements": ["terncy==0.3.5","websockets==8.1","zeroconf==0.28.8"], + "zeroconf": [ + {"type":"_websocket._tcp.local.","name":"box-*"}], + "dependencies": [], + "after_dependencies": ["discovery"], + "codeowners": [ + "@rxwen" + ] +} diff --git a/strings.json b/strings.json new file mode 100644 index 0000000..d6e3212 --- /dev/null +++ b/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/translations/en.json b/translations/en.json new file mode 100644 index 0000000..fd66d16 --- /dev/null +++ b/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "This Home Center has already been configured." + }, + "error": { + "invalid_auth": "Please approve access request in Terncy app first", + "need_new_auth": "Request a new token, then approve access request in Terncy app first, then submit again" + }, + "flow_title": "Terncy: {name}", + "step": { + "user": { + "data": { + "device": "Device" + } + }, + "confirm": { + "description": "You are about to add the `{name}` to Home Assistant.\n\n**To complete the process, you may have to approve access request in Terncy App after you submit access request.**\n\n", + "title": "Confirm adding Terncy Home Center" + }, + "begin_pairing": { + "description": "You are adding the `{name}` to Home Assistant.\n\n**To complete the process, please approve the access request in Terncy App. Then submit again.**\n\n", + "title": "Adding Terncy Home Center" + } + } + } +}