Initial Commit
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
"""The Cync Room Lights integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from .const import DOMAIN
|
||||
from .cync_hub import CyncHub
|
||||
|
||||
PLATFORMS: list[str] = ["light","binary_sensor","switch","fan"]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Cync Room Lights from a config entry."""
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
remove_options_update_listener = entry.add_update_listener(options_update_listener)
|
||||
hub = CyncHub(entry.data, entry.options, remove_options_update_listener)
|
||||
hass.data[DOMAIN][entry.entry_id] = hub
|
||||
hub.start_tcp_client()
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
async def options_update_listener(
|
||||
hass: HomeAssistant, config_entry: config_entries.ConfigEntry
|
||||
):
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hub = hass.data[DOMAIN][entry.entry_id]
|
||||
hub.remove_options_update_listener()
|
||||
hub.disconnect()
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,123 @@
|
||||
"""Platform for binary sensor integration."""
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDeviceClass, BinarySensorEntity)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from .const import DOMAIN
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
new_devices = []
|
||||
for sensor in hub.cync_motion_sensors:
|
||||
if not hub.cync_motion_sensors[sensor]._update_callback and sensor in config_entry.options["motion_sensors"]:
|
||||
new_devices.append(CyncMotionSensorEntity(hub.cync_motion_sensors[sensor]))
|
||||
for sensor in hub.cync_ambient_light_sensors:
|
||||
if not hub.cync_ambient_light_sensors[sensor]._update_callback and sensor in config_entry.options["ambient_light_sensors"]:
|
||||
new_devices.append(CyncAmbientLightSensorEntity(hub.cync_ambient_light_sensors[sensor]))
|
||||
|
||||
if new_devices:
|
||||
async_add_entities(new_devices)
|
||||
|
||||
|
||||
class CyncMotionSensorEntity(BinarySensorEntity):
|
||||
"""Representation of a Cync Motion Sensor."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, motion_sensor) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.motion_sensor = motion_sensor
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.motion_sensor.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.motion_sensor.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.motion_sensor.room.name} ({self.motion_sensor.home_name})",
|
||||
suggested_area = f"{self.motion_sensor.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
return 'cync_motion_sensor_' + self.motion_sensor.device_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the motion_sensor."""
|
||||
return self.motion_sensor.name + " Motion"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if light is on."""
|
||||
return self.motion_sensor.motion
|
||||
|
||||
@property
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the device class"""
|
||||
return BinarySensorDeviceClass.MOTION
|
||||
|
||||
class CyncAmbientLightSensorEntity(BinarySensorEntity):
|
||||
"""Representation of a Cync Ambient Light Sensor."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, ambient_light_sensor) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.ambient_light_sensor = ambient_light_sensor
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.ambient_light_sensor.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.ambient_light_sensor.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.ambient_light_sensor.room.name} ({self.ambient_light_sensor.home_name})",
|
||||
suggested_area = f"{self.ambient_light_sensor.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
return 'cync_ambient_light_sensor_' + self.ambient_light_sensor.device_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the ambient_light_sensor."""
|
||||
return self.ambient_light_sensor.name + " Ambient Light"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if light is on."""
|
||||
return self.ambient_light_sensor.ambient_light
|
||||
|
||||
@property
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the device class"""
|
||||
return BinarySensorDeviceClass.LIGHT
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
"""Config flow for Cync Room Lights integration."""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
from typing import Any
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.core import callback
|
||||
from .const import DOMAIN
|
||||
from .cync_hub import CyncUserData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("username"): str,
|
||||
vol.Required("password"): str,
|
||||
}
|
||||
)
|
||||
STEP_TWO_FACTOR_CODE = vol.Schema(
|
||||
{
|
||||
vol.Required("two_factor_code"): str,
|
||||
}
|
||||
)
|
||||
|
||||
async def cync_login(hub, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input"""
|
||||
|
||||
response = await hub.authenticate(user_input["username"], user_input["password"])
|
||||
if response['authorized']:
|
||||
return {'title':'cync_lights_'+ user_input['username'],'data':{'cync_credentials': hub.auth_code, 'user_input':user_input}}
|
||||
else:
|
||||
if response['two_factor_code_required']:
|
||||
raise TwoFactorCodeRequired
|
||||
else:
|
||||
raise InvalidAuth
|
||||
|
||||
async def submit_two_factor_code(hub, user_input: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the two factor code"""
|
||||
|
||||
response = await hub.auth_two_factor(user_input["two_factor_code"])
|
||||
if response['authorized']:
|
||||
return {'title':'cync_lights_'+ hub.username,'data':{'cync_credentials': hub.auth_code, 'user_input': {'username':hub.username,'password':hub.password}}}
|
||||
else:
|
||||
raise InvalidAuth
|
||||
|
||||
class CyncConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Cync Room Lights."""
|
||||
|
||||
def __init__(self):
|
||||
self.cync_hub = CyncUserData()
|
||||
self.data ={}
|
||||
self.options = {}
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user and password for Cync account."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await cync_login(self.cync_hub, user_input)
|
||||
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||
except TwoFactorCodeRequired:
|
||||
return await self.async_step_two_factor_code()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.error(e)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.data = info
|
||||
return await self.async_step_finish_setup()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_two_factor_code(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle two factor authentication for Cync account."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await submit_two_factor_code(self.cync_hub, user_input)
|
||||
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.error(e)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.data = info
|
||||
return await self.async_step_select_switches()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
|
||||
async def async_step_select_switches(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Select rooms and individual switches for entity creation"""
|
||||
if user_input is not None:
|
||||
self.options = user_input
|
||||
return await self._async_finish_setup()
|
||||
|
||||
switches_data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
"rooms",
|
||||
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
|
||||
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||
vol.Optional(
|
||||
"subgroups",
|
||||
description = {"suggested_value" : [room for room in self.data["data"]["cync_config"]["rooms"].keys() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']]},
|
||||
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.data["data"]["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||
vol.Optional(
|
||||
"switches",
|
||||
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['FAN']]},
|
||||
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.data["data"]["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
|
||||
vol.Optional(
|
||||
"motion_sensors",
|
||||
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['MOTION']]},
|
||||
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
|
||||
vol.Optional(
|
||||
"ambient_light_sensors",
|
||||
description = {"suggested_value" : [device_id for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info['AMBIENT_LIGHT']]},
|
||||
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.data["data"]["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
|
||||
|
||||
async def _async_finish_setup(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Finish setup and create entry"""
|
||||
|
||||
existing_entry = await self.async_set_unique_id(self.data['title'])
|
||||
if not existing_entry:
|
||||
return self.async_create_entry(title=self.data["title"], data=self.data["data"], options=self.options)
|
||||
else:
|
||||
self.hass.config_entries.async_update_entry(existing_entry, data=self.data['data'], options=self.options)
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.hass.config_entries.async_abort(reason="reauth_successful")
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return CyncOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class CyncOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.entry = config_entry
|
||||
self.cync_hub = CyncUserData()
|
||||
self.data = {}
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
|
||||
if user_input is not None:
|
||||
if user_input['re-authenticate'] == "No":
|
||||
return await self.async_step_select_switches()
|
||||
else:
|
||||
return await self.async_step_auth()
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
"re-authenticate",default="No"): vol.In(["Yes","No"]),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
|
||||
async def async_step_auth(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Attempt to re-authenticate"""
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await cync_login(self.cync_hub, self.entry.data['user_input'])
|
||||
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||
except TwoFactorCodeRequired:
|
||||
return await self.async_step_two_factor_code()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.error(e)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.data = info
|
||||
return await self.async_step_select_switches()
|
||||
|
||||
async def async_step_two_factor_code(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle two factor authentication for Cync account."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await submit_two_factor_code(self.cync_hub, user_input)
|
||||
info["data"]["cync_config"] = await self.cync_hub.get_cync_config()
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.error(e)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.data = info
|
||||
return await self.async_step_select_switches()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="two_factor_code", data_schema=STEP_TWO_FACTOR_CODE, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_select_switches(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
|
||||
if "data" in self.data and self.data["data"] != self.entry.data:
|
||||
self.hass.config_entries.async_update_entry(self.entry, data = self.data["data"])
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="",data=user_input)
|
||||
|
||||
switches_data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
"rooms",
|
||||
description = {"suggested_value" : [room for room in self.entry.options["rooms"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
|
||||
): cv.multi_select({room : f'{room_info["name"]} ({room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if not self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||
vol.Optional(
|
||||
"subgroups",
|
||||
description = {"suggested_value" : [room for room in self.entry.options["subgroups"] if room in self.entry.data["cync_config"]["rooms"].keys()]},
|
||||
): cv.multi_select({room : f'{room_info["name"]} ({room_info.get("parent_room","")}:{room_info["home_name"]})' for room,room_info in self.entry.data["cync_config"]["rooms"].items() if self.data["data"]["cync_config"]["rooms"][room]['isSubgroup']}),
|
||||
vol.Optional(
|
||||
"switches",
|
||||
description = {"suggested_value" : [sw for sw in self.entry.options["switches"] if sw in self.entry.data["cync_config"]["devices"].keys()]},
|
||||
): cv.multi_select({switch_id : f'{sw_info["name"]} ({sw_info["room_name"]}:{sw_info["home_name"]})' for switch_id,sw_info in self.entry.data["cync_config"]["devices"].items() if sw_info.get('ONOFF',False) and sw_info.get('MULTIELEMENT',1) == 1}),
|
||||
vol.Optional(
|
||||
"motion_sensors",
|
||||
description = {"suggested_value" : [sensor for sensor in self.entry.options["motion_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
|
||||
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('MOTION',False)}),
|
||||
vol.Optional(
|
||||
"ambient_light_sensors",
|
||||
description = {"suggested_value" : [sensor for sensor in self.entry.options["ambient_light_sensors"] if sensor in self.entry.data["cync_config"]["devices"].keys()]},
|
||||
): cv.multi_select({device_id : f'{device_info["name"]} ({device_info["room_name"]}:{device_info["home_name"]})' for device_id,device_info in self.entry.data["cync_config"]["devices"].items() if device_info.get('AMBIENT_LIGHT',False)}),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="select_switches", data_schema=switches_data_schema)
|
||||
|
||||
class TwoFactorCodeRequired(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Constants for the Cync Room Lights integration."""
|
||||
|
||||
DOMAIN = "cync_lights"
|
||||
@@ -0,0 +1,847 @@
|
||||
import logging
|
||||
import threading
|
||||
import asyncio
|
||||
import struct
|
||||
import aiohttp
|
||||
import math
|
||||
import ssl
|
||||
from typing import Any
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_AUTH = "https://api.gelighting.com/v2/user_auth"
|
||||
API_REQUEST_CODE = "https://api.gelighting.com/v2/two_factor/email/verifycode"
|
||||
API_2FACTOR_AUTH = "https://api.gelighting.com/v2/user_auth/two_factor"
|
||||
API_DEVICES = "https://api.gelighting.com/v2/user/{user}/subscribe/devices"
|
||||
API_DEVICE_INFO = "https://api.gelighting.com/v2/product/{product_id}/device/{device_id}/property"
|
||||
|
||||
Capabilities = {
|
||||
"ONOFF":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,47,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171,172],
|
||||
"BRIGHTNESS":[1,5,6,7,8,9,10,11,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,47,48,49,55,56,80,81,82,83,85,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
|
||||
"COLORTEMP":[5,6,7,8,10,11,14,15,19,20,21,22,23,25,26,28,29,30,31,32,33,34,35,47,80,82,83,85,129,130,131,132,133,135,136,137,138,139,140,141,142,143,144,145,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
|
||||
"RGB":[6,7,8,21,22,23,30,31,32,33,34,35,47,131,132,133,137,138,139,140,141,142,143,146,147,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171],
|
||||
"MOTION":[37,49,54],
|
||||
"AMBIENT_LIGHT":[37,49,54],
|
||||
"WIFICONTROL":[36,37,38,39,40,47,48,49,51,52,53,54,55,56,57,58,59,61,62,63,64,65,66,67,68,80,81,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,158,159,160,161,162,163,164,165,166,169,170,171,172],
|
||||
"PLUG":[64,65,66,67,68,172],
|
||||
"FAN":[81],
|
||||
"MULTIELEMENT":{'67':2}
|
||||
}
|
||||
|
||||
class CyncHub:
|
||||
|
||||
def __init__(self, user_data, options, remove_options_update_listener):
|
||||
|
||||
self.thread = None
|
||||
self.loop = None
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.login_code = bytearray(user_data['cync_credentials'])
|
||||
self.logged_in = False
|
||||
self.home_devices = user_data['cync_config']['home_devices']
|
||||
self.home_controllers = user_data['cync_config']['home_controllers']
|
||||
self.switchID_to_homeID = user_data['cync_config']['switchID_to_homeID']
|
||||
self.connected_devices = {home_id:[] for home_id in self.home_controllers.keys()}
|
||||
self.shutting_down = False
|
||||
self.remove_options_update_listener = remove_options_update_listener
|
||||
self.cync_rooms = {room_id:CyncRoom(room_id,room_info,self) for room_id,room_info in user_data['cync_config']['rooms'].items()}
|
||||
self.cync_switches = {device_id:CyncSwitch(device_id,switch_info,self.cync_rooms.get(switch_info['room'], None),self) for device_id,switch_info in user_data['cync_config']['devices'].items() if switch_info.get("ONOFF",False)}
|
||||
self.cync_motion_sensors = {device_id:CyncMotionSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("MOTION",False)}
|
||||
self.cync_ambient_light_sensors = {device_id:CyncAmbientLightSensor(device_id,device_info,self.cync_rooms.get(device_info['room'], None)) for device_id,device_info in user_data['cync_config']['devices'].items() if device_info.get("AMBIENT_LIGHT",False)}
|
||||
self.switchID_to_deviceIDs = {device_info.switch_id:[dev_id for dev_id, dev_info in self.cync_switches.items() if dev_info.switch_id == device_info.switch_id] for device_id, device_info in self.cync_switches.items() if int(device_info.switch_id) > 0}
|
||||
self.connected_devices_updated = False
|
||||
self.options = options
|
||||
self._seq_num = 0
|
||||
self.pending_commands = {}
|
||||
[room.initialize() for room in self.cync_rooms.values() if room.is_subgroup]
|
||||
[room.initialize() for room in self.cync_rooms.values() if not room.is_subgroup]
|
||||
|
||||
def start_tcp_client(self):
|
||||
self.thread = threading.Thread(target=self._start_tcp_client,daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def _start_tcp_client(self):
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_until_complete(self._connect())
|
||||
|
||||
def disconnect(self):
|
||||
self.shutting_down = True
|
||||
for home_controllers in self.home_controllers.values(): #send packets to server to generate data to be read which will initiate shutdown
|
||||
for controller in home_controllers:
|
||||
seq = self.get_seq_num()
|
||||
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,state_request)
|
||||
|
||||
async def _connect(self):
|
||||
while not self.shutting_down:
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
|
||||
except Exception as e:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23779, ssl = context)
|
||||
except Exception as e:
|
||||
self.reader, self.writer = await asyncio.open_connection('cm.gelighting.com', 23778)
|
||||
except Exception as e:
|
||||
_LOGGER.error(e)
|
||||
await asyncio.sleep(5)
|
||||
else:
|
||||
read_tcp_messages = asyncio.create_task(self._read_tcp_messages(), name = "Read TCP Messages")
|
||||
maintain_connection = asyncio.create_task(self._maintain_connection(), name = "Maintain Connection")
|
||||
update_state = asyncio.create_task(self._update_state(), name = "Update State")
|
||||
update_connected_devices = asyncio.create_task(self._update_connected_devices(), name = "Update Connected Devices")
|
||||
read_write_tasks = [read_tcp_messages, maintain_connection, update_state, update_connected_devices]
|
||||
try:
|
||||
done, pending = await asyncio.wait(read_write_tasks,return_when=asyncio.FIRST_EXCEPTION)
|
||||
for task in done:
|
||||
name = task.get_name()
|
||||
exception = task.exception()
|
||||
try:
|
||||
result = task.result()
|
||||
except Exception as e:
|
||||
_LOGGER.error(e)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
if not self.shutting_down:
|
||||
_LOGGER.info("Connection to Cync server reset, restarting in 15 seconds")
|
||||
await asyncio.sleep(15)
|
||||
else:
|
||||
_LOGGER.info("Cync client shutting down")
|
||||
except Exception as e:
|
||||
_LOGGER.error(e)
|
||||
|
||||
async def _read_tcp_messages(self):
|
||||
self.writer.write(self.login_code)
|
||||
await self.writer.drain()
|
||||
await self.reader.read(1000)
|
||||
self.logged_in = True
|
||||
while not self.shutting_down:
|
||||
data = await self.reader.read(1000)
|
||||
if len(data) == 0:
|
||||
self.logged_in = False
|
||||
raise LostConnection
|
||||
while len(data) >= 12:
|
||||
packet_type = int(data[0])
|
||||
packet_length = struct.unpack(">I", data[1:5])[0]
|
||||
packet = data[5:packet_length+5]
|
||||
try:
|
||||
if packet_length == len(packet):
|
||||
if packet_type == 115:
|
||||
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||
home_id = self.switchID_to_homeID[switch_id]
|
||||
|
||||
#send response packet
|
||||
response_id = struct.unpack(">H", packet[4:6])[0]
|
||||
response_packet = bytes.fromhex('7300000007') + int(switch_id).to_bytes(4,'big') + response_id.to_bytes(2,'big') + bytes.fromhex('00')
|
||||
self.loop.call_soon_threadsafe(self.send_request, response_packet)
|
||||
|
||||
if packet_length >= 33 and int(packet[13]) == 219:
|
||||
#parse state and brightness change packet
|
||||
deviceID = self.home_devices[home_id][int(packet[21])]
|
||||
state = int(packet[27]) > 0
|
||||
brightness = int(packet[28]) if state else 0
|
||||
if deviceID in self.cync_switches:
|
||||
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp,self.cync_switches[deviceID].rgb)
|
||||
elif packet_length >= 25 and int(packet[13]) == 84:
|
||||
#parse motion and ambient light sensor packet
|
||||
deviceID = self.home_devices[home_id][int(packet[16])]
|
||||
motion = int(packet[22]) > 0
|
||||
ambient_light = int(packet[24]) > 0
|
||||
if deviceID in self.cync_motion_sensors:
|
||||
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
|
||||
if deviceID in self.cync_ambient_light_sensors:
|
||||
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
|
||||
elif packet_length > 51 and int(packet[13]) == 82:
|
||||
#parse initial state packet
|
||||
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||
home_id = self.switchID_to_homeID[switch_id]
|
||||
self._add_connected_devices(switch_id, home_id)
|
||||
packet = packet[22:]
|
||||
while len(packet) > 24:
|
||||
deviceID = self.home_devices[home_id][int(packet[0])]
|
||||
if deviceID in self.cync_switches:
|
||||
if self.cync_switches[deviceID].elements > 1:
|
||||
for i in range(self.cync_switches[deviceID].elements):
|
||||
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[0])]
|
||||
state = int((int(packet[12]) >> i) & int(packet[8])) > 0
|
||||
brightness = 100 if state else 0
|
||||
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp,self.cync_switches[device_id].rgb)
|
||||
else:
|
||||
state = int(packet[8]) > 0
|
||||
brightness = int(packet[12]) if state else 0
|
||||
color_temp = int(packet[16])
|
||||
rgb = {'r':int(packet[20]),'g':int(packet[21]),'b':int(packet[22]),'active':int(packet[16])==254}
|
||||
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
|
||||
packet = packet[24:]
|
||||
elif packet_type == 131:
|
||||
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||
home_id = self.switchID_to_homeID[switch_id]
|
||||
if packet_length >= 33 and int(packet[13]) == 219:
|
||||
#parse state and brightness change packet
|
||||
deviceID = self.home_devices[home_id][int(packet[21])]
|
||||
state = int(packet[27]) > 0
|
||||
brightness = int(packet[28]) if state else 0
|
||||
if deviceID in self.cync_switches:
|
||||
self.cync_switches[deviceID].update_switch(state,brightness,self.cync_switches[deviceID].color_temp,self.cync_switches[deviceID].rgb)
|
||||
elif packet_length >= 25 and int(packet[13]) == 84:
|
||||
#parse motion and ambient light sensor packet
|
||||
deviceID = self.home_devices[home_id][int(packet[16])]
|
||||
motion = int(packet[22]) > 0
|
||||
ambient_light = int(packet[24]) > 0
|
||||
if deviceID in self.cync_motion_sensors:
|
||||
self.cync_motion_sensors[deviceID].update_motion_sensor(motion)
|
||||
if deviceID in self.cync_ambient_light_sensors:
|
||||
self.cync_ambient_light_sensors[deviceID].update_ambient_light_sensor(ambient_light)
|
||||
elif packet_type == 67 and packet_length >= 26 and int(packet[4]) == 1 and int(packet[5]) == 1 and int(packet[6]) == 6:
|
||||
#parse state packet
|
||||
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||
home_id = self.switchID_to_homeID[switch_id]
|
||||
packet = packet[7:]
|
||||
while len(packet) >= 19:
|
||||
if int(packet[3]) < len(self.home_devices[home_id]):
|
||||
deviceID = self.home_devices[home_id][int(packet[3])]
|
||||
if deviceID in self.cync_switches:
|
||||
if self.cync_switches[deviceID].elements > 1:
|
||||
for i in range(self.cync_switches[deviceID].elements):
|
||||
device_id = self.home_devices[home_id][(i+1)*256 + int(packet[3])]
|
||||
state = int((int(packet[5]) >> i) & int(packet[4])) > 0
|
||||
brightness = 100 if state else 0
|
||||
self.cync_switches[device_id].update_switch(state,brightness,self.cync_switches[device_id].color_temp,self.cync_switches[device_id].rgb)
|
||||
else:
|
||||
state = int(packet[4]) > 0
|
||||
brightness = int(packet[5]) if state else 0
|
||||
color_temp = int(packet[6])
|
||||
rgb = {'r':int(packet[7]),'g':int(packet[8]),'b':int(packet[9]),'active':int(packet[6])==254}
|
||||
self.cync_switches[deviceID].update_switch(state,brightness,color_temp,rgb)
|
||||
packet = packet[19:]
|
||||
elif packet_type == 171:
|
||||
switch_id = str(struct.unpack(">I", packet[0:4])[0])
|
||||
home_id = self.switchID_to_homeID[switch_id]
|
||||
self._add_connected_devices(switch_id, home_id)
|
||||
elif packet_type == 123:
|
||||
seq = str(struct.unpack(">H", packet[4:6])[0])
|
||||
command_received = self.pending_commands.get(seq,None)
|
||||
if command_received is not None:
|
||||
command_received(seq)
|
||||
except Exception as e:
|
||||
_LOGGER.error(e)
|
||||
data = data[packet_length+5:]
|
||||
raise ShuttingDown
|
||||
|
||||
|
||||
async def _maintain_connection(self):
|
||||
while not self.shutting_down:
|
||||
await asyncio.sleep(180)
|
||||
self.writer.write(bytes.fromhex('d300000000'))
|
||||
await self.writer.drain()
|
||||
raise ShuttingDown
|
||||
|
||||
def _add_connected_devices(self,switch_id, home_id):
|
||||
for dev in self.switchID_to_deviceIDs[switch_id]:
|
||||
#update list of WiFi connected devices
|
||||
if dev not in self.connected_devices[home_id]:
|
||||
self.connected_devices[home_id].append(dev)
|
||||
if self.connected_devices_updated:
|
||||
for dev in self.cync_switches.values():
|
||||
dev.update_controllers()
|
||||
for room in self.cync_rooms.values():
|
||||
room.update_controllers()
|
||||
|
||||
async def _update_connected_devices(self):
|
||||
while not self.shutting_down:
|
||||
self.connected_devices_updated = False
|
||||
for devices in self.connected_devices.values():
|
||||
devices.clear()
|
||||
while not self.logged_in:
|
||||
await asyncio.sleep(2)
|
||||
attempts = 0
|
||||
while True in [len(devices) < len(self.home_controllers[home_id]) * 0.5 for home_id,devices in self.connected_devices.items()] and attempts < 10:
|
||||
for home_id, home_controllers in self.home_controllers.items():
|
||||
for controller in home_controllers:
|
||||
seq = self.get_seq_num()
|
||||
ping = bytes.fromhex('a300000007') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('00')
|
||||
self.loop.call_soon_threadsafe(self.send_request, ping)
|
||||
await asyncio.sleep(0.15)
|
||||
await asyncio.sleep(2)
|
||||
attempts += 1
|
||||
for dev in self.cync_switches.values():
|
||||
dev.update_controllers()
|
||||
for room in self.cync_rooms.values():
|
||||
room.update_controllers()
|
||||
self.connected_devices_updated = True
|
||||
await asyncio.sleep(3600)
|
||||
raise ShuttingDown
|
||||
|
||||
async def _update_state(self):
|
||||
while not self.connected_devices_updated:
|
||||
await asyncio.sleep(2)
|
||||
for connected_devices in self.connected_devices.values():
|
||||
if len(connected_devices) > 0:
|
||||
controller = self.cync_switches[connected_devices[0]].switch_id
|
||||
seq = self.get_seq_num()
|
||||
state_request = bytes.fromhex('7300000018') + int(controller).to_bytes(4,'big') + seq.to_bytes(2,'big') + bytes.fromhex('007e00000000f85206000000ffff0000567e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,state_request)
|
||||
while False in [self.cync_switches[dev_id]._update_callback is not None for dev_id in self.options["switches"]] and False in [self.cync_rooms[dev_id]._update_callback is not None for dev_id in self.options["rooms"]]:
|
||||
await asyncio.sleep(2)
|
||||
for dev in self.cync_switches.values():
|
||||
dev.publish_update()
|
||||
for room in self.cync_rooms.values():
|
||||
dev.publish_update()
|
||||
|
||||
def send_request(self,request):
|
||||
async def send():
|
||||
self.writer.write(request)
|
||||
await self.writer.drain()
|
||||
self.loop.create_task(send())
|
||||
|
||||
def combo_control(self,state,brightness,color_tone,rgb,switch_id,mesh_id,seq):
|
||||
combo_request = bytes.fromhex('7300000022') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8f010000000000000') + mesh_id + bytes.fromhex('f00000') + (1 if state else 0).to_bytes(1,'big') + brightness.to_bytes(1,'big') + color_tone.to_bytes(1,'big') + rgb[0].to_bytes(1,'big') + rgb[1].to_bytes(1,'big') + rgb[2].to_bytes(1,'big') + ((496 + int(mesh_id[0]) + int(mesh_id[1]) + (1 if state else 0) + brightness + color_tone + sum(rgb))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,combo_request)
|
||||
|
||||
def turn_on(self,switch_id,mesh_id,seq):
|
||||
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000010000') + ((430 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,power_request)
|
||||
|
||||
def turn_off(self,switch_id,mesh_id,seq):
|
||||
power_request = bytes.fromhex('730000001f') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8d00d000000000000') + mesh_id + bytes.fromhex('d00000000000') + ((429 + int(mesh_id[0]) + int(mesh_id[1]))%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,power_request)
|
||||
|
||||
def set_color_temp(self,color_temp,switch_id,mesh_id,seq):
|
||||
color_temp_request = bytes.fromhex('730000001e') + int(switch_id).to_bytes(4,'big') + int(seq).to_bytes(2,'big') + bytes.fromhex('007e00000000f8e20c000000000000') + mesh_id + bytes.fromhex('e2000005') + color_temp.to_bytes(1,'big') + ((469 + int(mesh_id[0]) + int(mesh_id[1]) + color_temp)%256).to_bytes(1,'big') + bytes.fromhex('7e')
|
||||
self.loop.call_soon_threadsafe(self.send_request,color_temp_request)
|
||||
|
||||
def get_seq_num(self):
|
||||
if self._seq_num == 65535:
|
||||
self._seq_num = 1
|
||||
else:
|
||||
self._seq_num += 1
|
||||
return self._seq_num
|
||||
|
||||
class CyncRoom:
|
||||
|
||||
def __init__(self, room_id, room_info, hub):
|
||||
|
||||
self.hub = hub
|
||||
self.room_id = room_id
|
||||
self.home_id = room_id.split('-')[0]
|
||||
self.name = room_info.get('name','unknown')
|
||||
self.home_name = room_info.get('home_name','unknown')
|
||||
self.parent_room = room_info.get('parent_room', 'unknown')
|
||||
self.mesh_id = int(room_info.get('mesh_id',0)).to_bytes(2,'little')
|
||||
self.power_state = False
|
||||
self.brightness = 0
|
||||
self.color_temp = 0
|
||||
self.rgb = {'r':0, 'g':0, 'b':0, 'active': False}
|
||||
self.switches = room_info.get('switches',[])
|
||||
self.subgroups = room_info.get('subgroups',[])
|
||||
self.is_subgroup = room_info.get('isSubgroup', False)
|
||||
self.all_room_switches = self.switches
|
||||
self.controllers = []
|
||||
self.default_controller = room_info.get('room_controller',self.hub.home_controllers[self.home_id][0])
|
||||
self._update_callback = None
|
||||
self._update_parent_room = None
|
||||
self.support_brightness = False
|
||||
self.support_color_temp = False
|
||||
self.support_rgb = False
|
||||
self.switches_support_brightness = False
|
||||
self.switches_support_color_temp = False
|
||||
self.switches_support_rgb = False
|
||||
self.groups_support_brightness = False
|
||||
self.groups_support_color_temp = False
|
||||
self.groups_support_rgb = False
|
||||
self._command_timout = 0.5
|
||||
self._command_retry_time = 5
|
||||
|
||||
def initialize(self):
|
||||
"""Initialization of supported features and registration of update function for all switches and subgroups in the room"""
|
||||
self.switches_support_brightness = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_brightness]
|
||||
self.switches_support_color_temp = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_color_temp]
|
||||
self.switches_support_rgb = [device_id for device_id in self.switches if self.hub.cync_switches[device_id].support_rgb]
|
||||
self.groups_support_brightness = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_brightness]
|
||||
self.groups_support_color_temp = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_color_temp]
|
||||
self.groups_support_rgb = [room_id for room_id in self.subgroups if self.hub.cync_rooms[room_id].support_rgb]
|
||||
self.support_brightness = (len(self.switches_support_brightness) + len(self.groups_support_brightness)) > 0
|
||||
self.support_color_temp = (len(self.switches_support_color_temp) + len(self.groups_support_color_temp)) > 0
|
||||
self.support_rgb = (len(self.switches_support_rgb) + len(self.groups_support_rgb)) > 0
|
||||
for switch_id in self.switches:
|
||||
self.hub.cync_switches[switch_id].register_room_updater(self.update_room)
|
||||
for subgroup in self.subgroups:
|
||||
self.hub.cync_rooms[subgroup].register_room_updater(self.update_room)
|
||||
self.all_room_switches = self.all_room_switches + self.hub.cync_rooms[subgroup].switches
|
||||
for subgroup in self.subgroups:
|
||||
self.hub.cync_rooms[subgroup].all_room_switches = self.all_room_switches
|
||||
|
||||
def register(self, update_callback) -> None:
|
||||
"""Register callback, called when switch changes state."""
|
||||
self._update_callback = update_callback
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Remove previously registered callback."""
|
||||
self._update_callback = None
|
||||
|
||||
def register_room_updater(self, parent_updater):
|
||||
self._update_parent_room = parent_updater
|
||||
|
||||
@property
|
||||
def max_color_temp_kelvin(self) -> int:
|
||||
"""Return maximum supported color temperature."""
|
||||
return 7000
|
||||
|
||||
@property
|
||||
def min_color_temp_kelvin(self) -> int:
|
||||
"""Return minimum supported color temperature."""
|
||||
return 2000
|
||||
|
||||
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
|
||||
"""Turn on the light."""
|
||||
attempts = 0
|
||||
update_received = False
|
||||
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||
seq = str(self.hub.get_seq_num())
|
||||
if len(self.controllers) > 0:
|
||||
controller = self.controllers[attempts%len(self.controllers)]
|
||||
else:
|
||||
controller = self.default_controller
|
||||
if attr_rgb is not None and attr_br is not None:
|
||||
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
|
||||
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||
else:
|
||||
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||
elif attr_rgb is None and attr_ct is None and attr_br is not None:
|
||||
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||
elif attr_rgb is not None and attr_br is None:
|
||||
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||
elif attr_ct is not None:
|
||||
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||
# Cync uses the range 0% to 100% to set the color temp, so we need to
|
||||
# calculate the percentage of the color temp range that is being requested
|
||||
color_temp = round(100*((attr_ct - self.min_color_temp_kelvin) / (self.max_color_temp_kelvin - self.min_color_temp_kelvin)))
|
||||
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
|
||||
else:
|
||||
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||
self.hub.pending_commands[seq] = self.command_received
|
||||
await asyncio.sleep(self._command_timout)
|
||||
if self.hub.pending_commands.get(seq, None) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
attempts += 1
|
||||
else:
|
||||
update_received = True
|
||||
|
||||
async def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
attempts = 0
|
||||
update_received = False
|
||||
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||
seq = str(self.hub.get_seq_num())
|
||||
if len(self.controllers) > 0:
|
||||
controller = self.controllers[attempts%len(self.controllers)]
|
||||
else:
|
||||
controller = self.default_controller
|
||||
self.hub.turn_off(controller, self.mesh_id, seq)
|
||||
self.hub.pending_commands[seq] = self.command_received
|
||||
await asyncio.sleep(self._command_timout)
|
||||
if self.hub.pending_commands.get(seq, None) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
attempts += 1
|
||||
else:
|
||||
update_received = True
|
||||
|
||||
def command_received(self, seq):
|
||||
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
|
||||
if self.hub.pending_commands.get(seq) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
|
||||
def update_room(self):
|
||||
"""Update the current state of the room"""
|
||||
_brightness = self.brightness
|
||||
_color_temp = self.color_temp
|
||||
_rgb = self.rgb
|
||||
_power_state = True in ([self.hub.cync_switches[device_id].power_state for device_id in self.switches] + [self.hub.cync_rooms[room_id].power_state for room_id in self.subgroups])
|
||||
if self.support_brightness:
|
||||
_brightness = round(sum([self.hub.cync_switches[device_id].brightness for device_id in self.switches] + [self.hub.cync_rooms[room_id].brightness for room_id in self.subgroups])/(len(self.switches) + len(self.subgroups)))
|
||||
else:
|
||||
_brightness = 100 if _power_state else 0
|
||||
if self.support_color_temp:
|
||||
_color_temp = round(sum([self.hub.cync_switches[device_id].color_temp for device_id in self.switches_support_color_temp] + [self.hub.cync_rooms[room_id].color_temp for room_id in self.groups_support_color_temp])/(len(self.switches_support_color_temp) + len(self.groups_support_color_temp)))
|
||||
if self.support_rgb:
|
||||
_rgb['r'] = round(sum([self.hub.cync_switches[device_id].rgb['r'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['r'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||
_rgb['g'] = round(sum([self.hub.cync_switches[device_id].rgb['g'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['g'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||
_rgb['b'] = round(sum([self.hub.cync_switches[device_id].rgb['b'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['b'] for room_id in self.groups_support_rgb])/(len(self.switches_support_rgb) + len(self.groups_support_rgb)))
|
||||
_rgb['active'] = True in ([self.hub.cync_switches[device_id].rgb['active'] for device_id in self.switches_support_rgb] + [self.hub.cync_rooms[room_id].rgb['active'] for room_id in self.groups_support_rgb])
|
||||
if _power_state != self.power_state or _brightness != self.brightness or _color_temp != self.color_temp or _rgb != self.rgb:
|
||||
self.power_state = _power_state
|
||||
self.brightness = _brightness
|
||||
self.color_temp = _color_temp
|
||||
self.rgb = _rgb
|
||||
self.publish_update()
|
||||
if self._update_parent_room:
|
||||
self._update_parent_room()
|
||||
|
||||
def update_controllers(self):
|
||||
"""Update the list of responsive, Wi-Fi connected controller devices"""
|
||||
connected_devices = self.hub.connected_devices[self.home_id]
|
||||
controllers = []
|
||||
if len(connected_devices) > 0:
|
||||
controllers = [self.hub.cync_switches[dev_id].switch_id for dev_id in self.all_room_switches if dev_id in connected_devices]
|
||||
others_available = [self.hub.cync_switches[dev_id].switch_id for dev_id in connected_devices]
|
||||
for controller in controllers:
|
||||
if controller in others_available:
|
||||
others_available.remove(controller)
|
||||
self.controllers = controllers + others_available
|
||||
else:
|
||||
self.controllers = [self.default_controller]
|
||||
|
||||
def publish_update(self):
|
||||
if self._update_callback:
|
||||
self._update_callback()
|
||||
|
||||
class CyncSwitch:
|
||||
|
||||
def __init__(self, device_id, switch_info, room, hub):
|
||||
self.hub = hub
|
||||
self.device_id = device_id
|
||||
self.switch_id = switch_info.get('switch_id','0')
|
||||
self.home_id = [home_id for home_id, home_devices in self.hub.home_devices.items() if self.device_id in home_devices][0]
|
||||
self.name = switch_info.get('name','unknown')
|
||||
self.home_name = switch_info.get('home_name','unknown')
|
||||
self.mesh_id = switch_info.get('mesh_id',0).to_bytes(2,'little')
|
||||
self.room = room
|
||||
self.power_state = False
|
||||
self.brightness = 0
|
||||
self.color_temp = 0
|
||||
self.rgb = {'r':0, 'g':0, 'b':0, 'active':False}
|
||||
self.default_controller = switch_info.get('switch_controller',self.hub.home_controllers[self.home_id][0])
|
||||
self.controllers = []
|
||||
self._update_callback = None
|
||||
self._update_parent_room = None
|
||||
self.support_brightness = switch_info.get('BRIGHTNESS',False)
|
||||
self.support_color_temp = switch_info.get('COLORTEMP',False)
|
||||
self.support_rgb = switch_info.get('RGB',False)
|
||||
self.plug = switch_info.get('PLUG',False)
|
||||
self.fan = switch_info.get('FAN',False)
|
||||
self.elements = switch_info.get('MULTIELEMENT',1)
|
||||
self._command_timout = 0.5
|
||||
self._command_retry_time = 5
|
||||
|
||||
def register(self, update_callback) -> None:
|
||||
"""Register callback, called when switch changes state."""
|
||||
self._update_callback = update_callback
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Remove previously registered callback."""
|
||||
self._update_callback = None
|
||||
|
||||
def register_room_updater(self, parent_updater):
|
||||
self._update_parent_room = parent_updater
|
||||
|
||||
@property
|
||||
def max_color_temp_kelvin(self) -> int:
|
||||
"""Return maximum supported color temperature."""
|
||||
return 7000
|
||||
|
||||
@property
|
||||
def min_color_temp_kelvin(self) -> int:
|
||||
"""Return minimum supported color temperature."""
|
||||
return 2000
|
||||
|
||||
async def turn_on(self, attr_rgb, attr_br, attr_ct) -> None:
|
||||
"""Turn on the light."""
|
||||
attempts = 0
|
||||
update_received = False
|
||||
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||
seq = str(self.hub.get_seq_num())
|
||||
if len(self.controllers) > 0:
|
||||
controller = self.controllers[attempts%len(self.controllers)]
|
||||
else:
|
||||
controller = self.default_controller
|
||||
if attr_rgb is not None and attr_br is not None:
|
||||
if math.isclose(attr_br, max([self.rgb['r'],self.rgb['g'],self.rgb['b']])*self.brightness/100, abs_tol = 2):
|
||||
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||
else:
|
||||
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||
elif attr_rgb is None and attr_ct is None and attr_br is not None:
|
||||
self.hub.combo_control(True, round(attr_br*100/255), 255, [255,255,255], controller, self.mesh_id, seq)
|
||||
elif attr_rgb is not None and attr_br is None:
|
||||
self.hub.combo_control(True, self.brightness, 254, attr_rgb, controller, self.mesh_id, seq)
|
||||
elif attr_ct is not None:
|
||||
# Cync uses the range 0% to 100% to set the color temp, so we need to
|
||||
# calculate the percentage of the color temp range that is being requested
|
||||
color_temp = round(100*((attr_ct - self.min_color_temp_kelvin) /(self.max_color_temp_kelvin - self.min_color_temp_kelvin)))
|
||||
self.hub.set_color_temp(color_temp, controller, self.mesh_id, seq)
|
||||
else:
|
||||
self.hub.turn_on(controller, self.mesh_id, seq)
|
||||
self.hub.pending_commands[seq] = self.command_received
|
||||
await asyncio.sleep(self._command_timout)
|
||||
if self.hub.pending_commands.get(seq, None) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
attempts += 1
|
||||
else:
|
||||
update_received = True
|
||||
|
||||
async def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
attempts = 0
|
||||
update_received = False
|
||||
while not update_received and attempts < int(self._command_retry_time/self._command_timout):
|
||||
seq = str(self.hub.get_seq_num())
|
||||
if len(self.controllers) > 0:
|
||||
controller = self.controllers[attempts%len(self.controllers)]
|
||||
else:
|
||||
controller = self.default_controller
|
||||
self.hub.turn_off(controller, self.mesh_id, seq)
|
||||
self.hub.pending_commands[seq] = self.command_received
|
||||
await asyncio.sleep(self._command_timout)
|
||||
if self.hub.pending_commands.get(seq, None) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
attempts += 1
|
||||
else:
|
||||
update_received = True
|
||||
|
||||
def command_received(self, seq):
|
||||
"""Remove command from hub.pending_commands when a reply is received from Cync server"""
|
||||
if self.hub.pending_commands.get(seq) is not None:
|
||||
self.hub.pending_commands.pop(seq)
|
||||
|
||||
def update_switch(self,state,brightness,color_temp,rgb):
|
||||
"""Update the state of the switch as updates are received from the Cync server"""
|
||||
self.update_received = True
|
||||
if self.power_state != state or self.brightness != brightness or self.color_temp != color_temp or self.rgb != rgb:
|
||||
self.power_state = state
|
||||
self.brightness = brightness if self.support_brightness and state else 100 if state else 0
|
||||
self.color_temp = color_temp
|
||||
self.rgb = rgb
|
||||
self.publish_update()
|
||||
if self._update_parent_room:
|
||||
self._update_parent_room()
|
||||
|
||||
def update_controllers(self):
|
||||
"""Update the list of responsive, Wi-Fi connected controller devices"""
|
||||
connected_devices = self.hub.connected_devices[self.home_id]
|
||||
controllers = []
|
||||
if len(connected_devices) > 0:
|
||||
if int(self.switch_id) > 0:
|
||||
if self.device_id in connected_devices:
|
||||
#if this device is connected, make this the first available controller
|
||||
controllers.append(self.switch_id)
|
||||
if self.room:
|
||||
controllers = controllers + [self.hub.cync_switches[device_id].switch_id for device_id in self.room.all_room_switches if device_id in connected_devices and device_id != self.device_id]
|
||||
others_available = [self.hub.cync_switches[device_id].switch_id for device_id in connected_devices]
|
||||
for controller in controllers:
|
||||
if controller in others_available:
|
||||
others_available.remove(controller)
|
||||
self.controllers = controllers + others_available
|
||||
else:
|
||||
self.controllers = [self.default_controller]
|
||||
|
||||
def publish_update(self):
|
||||
if self._update_callback:
|
||||
self._update_callback()
|
||||
|
||||
class CyncMotionSensor:
|
||||
|
||||
def __init__(self, device_id, device_info, room):
|
||||
|
||||
self.device_id = device_id
|
||||
self.name = device_info['name']
|
||||
self.home_name = device_info['home_name']
|
||||
self.room = room
|
||||
self.motion = False
|
||||
self._update_callback = None
|
||||
|
||||
def register(self, update_callback) -> None:
|
||||
"""Register callback, called when switch changes state."""
|
||||
self._update_callback = update_callback
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Remove previously registered callback."""
|
||||
self._update_callback = None
|
||||
|
||||
def update_motion_sensor(self,motion):
|
||||
self.motion = motion
|
||||
self.publish_update()
|
||||
|
||||
def publish_update(self):
|
||||
if self._update_callback:
|
||||
self._update_callback()
|
||||
|
||||
class CyncAmbientLightSensor:
|
||||
|
||||
def __init__(self, device_id, device_info, room):
|
||||
|
||||
self.device_id = device_id
|
||||
self.name = device_info['name']
|
||||
self.home_name = device_info['home_name']
|
||||
self.room = room
|
||||
self.ambient_light = False
|
||||
self._update_callback = None
|
||||
|
||||
def register(self, update_callback) -> None:
|
||||
"""Register callback, called when switch changes state."""
|
||||
self._update_callback = update_callback
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Remove previously registered callback."""
|
||||
self._update_callback = None
|
||||
|
||||
def update_ambient_light_sensor(self,ambient_light):
|
||||
self.ambient_light = ambient_light
|
||||
self.publish_update()
|
||||
|
||||
def publish_update(self):
|
||||
if self._update_callback:
|
||||
self._update_callback()
|
||||
|
||||
class CyncUserData:
|
||||
|
||||
def __init__(self):
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.auth_code = None
|
||||
self.user_credentials = {}
|
||||
|
||||
async def authenticate(self,username,password):
|
||||
"""Authenticate with the API and get a token."""
|
||||
self.username = username
|
||||
self.password = password
|
||||
auth_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'password': self.password}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(API_AUTH, json=auth_data) as resp:
|
||||
if resp.status == 200:
|
||||
self.user_credentials = await resp.json()
|
||||
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
|
||||
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
|
||||
return {'authorized':True}
|
||||
elif resp.status == 400:
|
||||
request_code_data = {'corp_id': "1007d2ad150c4000", 'email': self.username, 'local_lang': "en-us"}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(API_REQUEST_CODE,json=request_code_data) as resp:
|
||||
if resp.status == 200:
|
||||
return {'authorized':False,'two_factor_code_required':True}
|
||||
else:
|
||||
return {'authorized':False,'two_factor_code_required':False}
|
||||
else:
|
||||
return {'authorized':False,'two_factor_code_required':False}
|
||||
|
||||
async def auth_two_factor(self, code):
|
||||
"""Authenticate with 2 Factor Code."""
|
||||
two_factor_data = {'corp_id': "1007d2ad150c4000", 'email': self.username,'password': self.password, 'two_factor': code, 'resource':"abcdefghijklmnop"}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(API_2FACTOR_AUTH,json=two_factor_data) as resp:
|
||||
if resp.status == 200:
|
||||
self.user_credentials = await resp.json()
|
||||
login_code = bytearray.fromhex('13000000') + (10 + len(self.user_credentials['authorize'])).to_bytes(1,'big') + bytearray.fromhex('03') + self.user_credentials['user_id'].to_bytes(4,'big') + len(self.user_credentials['authorize']).to_bytes(2,'big') + bytearray(self.user_credentials['authorize'],'ascii') + bytearray.fromhex('0000b4')
|
||||
self.auth_code = [int.from_bytes([byt],'big') for byt in login_code]
|
||||
return {'authorized':True}
|
||||
else:
|
||||
return {'authorized':False}
|
||||
|
||||
async def get_cync_config(self):
|
||||
home_devices = {}
|
||||
home_controllers = {}
|
||||
switchID_to_homeID = {}
|
||||
devices = {}
|
||||
rooms = {}
|
||||
homes = await self._get_homes()
|
||||
for home in homes:
|
||||
home_info = await self._get_home_properties(home['product_id'], home['id'])
|
||||
if home_info.get('groupsArray',False) and home_info.get('bulbsArray',False) and len(home_info['groupsArray']) > 0 and len(home_info['bulbsArray']) > 0:
|
||||
home_id = str(home['id'])
|
||||
bulbs_array_length = max([((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256) for device in home_info['bulbsArray']]) + 1
|
||||
home_devices[home_id] = [""]*(bulbs_array_length)
|
||||
home_controllers[home_id] = []
|
||||
for device in home_info['bulbsArray']:
|
||||
device_type = device['deviceType']
|
||||
device_id = str(device['deviceID'])
|
||||
current_index = ((device['deviceID'] % home['id']) % 1000) + (int((device['deviceID'] % home['id']) / 1000)*256)
|
||||
home_devices[home_id][current_index] = device_id
|
||||
devices[device_id] = {'name':device['displayName'],
|
||||
'mesh_id':current_index,
|
||||
'switch_id':str(device.get('switchID',0)),
|
||||
'ONOFF': device_type in Capabilities['ONOFF'],
|
||||
'BRIGHTNESS': device_type in Capabilities["BRIGHTNESS"],
|
||||
"COLORTEMP":device_type in Capabilities["COLORTEMP"],
|
||||
"RGB": device_type in Capabilities["RGB"],
|
||||
"MOTION": device_type in Capabilities["MOTION"],
|
||||
"AMBIENT_LIGHT": device_type in Capabilities["AMBIENT_LIGHT"],
|
||||
"WIFICONTROL": device_type in Capabilities["WIFICONTROL"],
|
||||
"PLUG" : device_type in Capabilities["PLUG"],
|
||||
"FAN" : device_type in Capabilities["FAN"],
|
||||
'home_name':home['name'],
|
||||
'room':'',
|
||||
'room_name':''
|
||||
}
|
||||
if str(device_type) in Capabilities['MULTIELEMENT'] and current_index < 256:
|
||||
devices[device_id]['MULTIELEMENT'] = Capabilities['MULTIELEMENT'][str(device_type)]
|
||||
if devices[device_id].get('WIFICONTROL',False) and 'switchID' in device and device['switchID'] > 0:
|
||||
switchID_to_homeID[str(device['switchID'])] = home_id
|
||||
devices[device_id]['switch_controller'] = device['switchID']
|
||||
home_controllers[home_id].append(device['switchID'])
|
||||
if len(home_controllers[home_id]) == 0:
|
||||
for device in home_info['bulbsArray']:
|
||||
device_id = str(device['deviceID'])
|
||||
devices.pop(device_id,'')
|
||||
home_devices.pop(home_id,'')
|
||||
home_controllers.pop(home_id,'')
|
||||
else:
|
||||
for room in home_info['groupsArray']:
|
||||
if (len(room.get('deviceIDArray',[])) + len(room.get('subgroupIDArray',[]))) > 0:
|
||||
room_id = home_id + '-' + str(room['groupID'])
|
||||
room_controller = home_controllers[home_id][0]
|
||||
available_room_controllers = [(id%1000) + (int(id/1000)*256) for id in room.get('deviceIDArray',[]) if 'switch_controller' in devices[home_devices[home_id][(id%1000)+(int(id/1000)*256)]]]
|
||||
if len(available_room_controllers) > 0:
|
||||
room_controller = devices[home_devices[home_id][available_room_controllers[0]]]['switch_controller']
|
||||
for id in room.get('deviceIDArray',[]):
|
||||
id = (id % 1000) + (int(id / 1000)*256)
|
||||
devices[home_devices[home_id][id]]['room'] = room_id
|
||||
devices[home_devices[home_id][id]]['room_name'] = room['displayName']
|
||||
if 'switch_controller' not in devices[home_devices[home_id][id]] and devices[home_devices[home_id][id]].get('ONOFF',False):
|
||||
devices[home_devices[home_id][id]]['switch_controller'] = room_controller
|
||||
rooms[room_id] = {'name':room['displayName'],
|
||||
'mesh_id' : room['groupID'],
|
||||
'room_controller' : room_controller,
|
||||
'home_name' : home['name'],
|
||||
'switches' : [home_devices[home_id][(i%1000)+(int(i/1000)*256)] for i in room.get('deviceIDArray',[]) if devices[home_devices[home_id][(i%1000)+(int(i/1000)*256)]].get('ONOFF',False)],
|
||||
'isSubgroup' : room.get('isSubgroup',False),
|
||||
'subgroups' : [home_id + '-' + str(subgroup) for subgroup in room.get('subgroupIDArray',[])]
|
||||
}
|
||||
for room,room_info in rooms.items():
|
||||
if not room_info.get("isSubgroup",False) and len(subgroups := room_info.get("subgroups",[]).copy()) > 0:
|
||||
for subgroup in subgroups:
|
||||
if rooms.get(subgroup,None):
|
||||
rooms[subgroup]["parent_room"] = room_info["name"]
|
||||
else:
|
||||
room_info['subgroups'].pop(room_info['subgroups'].index(subgroup))
|
||||
|
||||
if len(rooms) == 0 or len(devices) == 0 or len(home_controllers) == 0 or len(home_devices) == 0 or len(switchID_to_homeID) == 0:
|
||||
raise InvalidCyncConfiguration
|
||||
else:
|
||||
return {'rooms':rooms, 'devices':devices, 'home_devices':home_devices, 'home_controllers':home_controllers, 'switchID_to_homeID':switchID_to_homeID}
|
||||
|
||||
async def _get_homes(self):
|
||||
"""Get a list of devices for a particular user."""
|
||||
headers = {'Access-Token': self.user_credentials['access_token']}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(API_DEVICES.format(user=self.user_credentials['user_id']), headers=headers) as resp:
|
||||
response = await resp.json()
|
||||
return response
|
||||
|
||||
async def _get_home_properties(self, product_id, device_id):
|
||||
"""Get properties for a single device."""
|
||||
headers = {'Access-Token': self.user_credentials['access_token']}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(API_DEVICE_INFO.format(product_id=product_id, device_id=device_id), headers=headers) as resp:
|
||||
response = await resp.json()
|
||||
return response
|
||||
|
||||
class LostConnection(Exception):
|
||||
"""Lost connection to Cync Server"""
|
||||
|
||||
class ShuttingDown(Exception):
|
||||
"""Cync client shutting down"""
|
||||
|
||||
class InvalidCyncConfiguration(Exception):
|
||||
"""Cync configuration is not supported"""
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Platform for light integration."""
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
new_devices = []
|
||||
for switch_id in hub.cync_switches:
|
||||
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
|
||||
new_devices.append(CyncFanEntity(hub.cync_switches[switch_id]))
|
||||
|
||||
if new_devices:
|
||||
async_add_entities(new_devices)
|
||||
|
||||
class CyncFanEntity(FanEntity):
|
||||
"""Representation of a Cync Fan Switch Entity."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, cync_switch) -> None:
|
||||
"""Initialize the light."""
|
||||
self.cync_switch = cync_switch
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.cync_switch.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.cync_switch.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||
suggested_area = f"{self.cync_switch.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
return 'cync_switch_' + self.cync_switch.device_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the switch."""
|
||||
return self.cync_switch.name
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return the list of supported features."""
|
||||
return FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if fan is on."""
|
||||
return self.cync_switch.power_state
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
"""Return the fan speed percentage of this switch"""
|
||||
return self.cync_switch.brightness
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
"""Return the number of speeds the fan supports."""
|
||||
return 4
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Turn on the light."""
|
||||
await self.cync_switch.turn_on(None,percentage*255/100 if percentage is not None else None,None)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.cync_switch.turn_off()
|
||||
|
||||
async def async_set_percentage(self, percentage: int) -> None:
|
||||
"""Set the speed of the fan, as a percentage."""
|
||||
if percentage == 0:
|
||||
await self.async_turn_off()
|
||||
else:
|
||||
await self.cync_switch.turn_on(None,percentage*255/100,None)
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Platform for light integration."""
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ColorMode, LightEntity)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from .const import DOMAIN
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
new_devices = []
|
||||
for room in hub.cync_rooms:
|
||||
if not hub.cync_rooms[room]._update_callback and (room in config_entry.options["rooms"] or room in config_entry.options["subgroups"]):
|
||||
new_devices.append(CyncRoomEntity(hub.cync_rooms[room]))
|
||||
|
||||
for switch_id in hub.cync_switches:
|
||||
if not hub.cync_switches[switch_id]._update_callback and not hub.cync_switches[switch_id].plug and not hub.cync_switches[switch_id].fan and switch_id in config_entry.options["switches"]:
|
||||
new_devices.append(CyncSwitchEntity(hub.cync_switches[switch_id]))
|
||||
|
||||
if new_devices:
|
||||
async_add_entities(new_devices)
|
||||
|
||||
|
||||
class CyncRoomEntity(LightEntity):
|
||||
"""Representation of a Cync Room Light Entity."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, room) -> None:
|
||||
"""Initialize the light."""
|
||||
self.room = room
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.room.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.room.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.room.parent_room if self.room.is_subgroup else self.room.name} ({self.room.home_name})",
|
||||
suggested_area = f"{self.room.parent_room if self.room.is_subgroup else self.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Icon of the entity."""
|
||||
if self.room.is_subgroup:
|
||||
return "mdi:lightbulb-group-outline"
|
||||
else:
|
||||
return "mdi:lightbulb-group"
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
uid = 'cync_room_' + '-'.join(self.room.switches) + '_' + '-'.join(self.room.subgroups)
|
||||
return uid
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the room."""
|
||||
return self.room.name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if light is on."""
|
||||
return self.room.power_state
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this room between 0..255."""
|
||||
return round(self.room.brightness*255/100)
|
||||
|
||||
@property
|
||||
def max_color_temp_kelvin(self) -> int:
|
||||
"""Return maximum supported color temperature."""
|
||||
return self.room.max_color_temp_kelvin
|
||||
|
||||
@property
|
||||
def min_color_temp_kelvin(self) -> int:
|
||||
"""Return minimum supported color temperature."""
|
||||
return self.room.min_color_temp_kelvin
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int:
|
||||
"""Return color temperature in kelvin."""
|
||||
return self.min_color_temp_kelvin + round((self.max_color_temp_kelvin-self.min_color_temp_kelvin)*self.room.color_temp/100)
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
"""Return the RGB color tuple of this light switch"""
|
||||
return (self.room.rgb['r'],self.room.rgb['g'],self.room.rgb['b'])
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[str] | None:
|
||||
"""Return list of available color modes."""
|
||||
|
||||
modes: set[ColorMode | str] = set()
|
||||
|
||||
if self.room.support_color_temp:
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
if self.room.support_rgb:
|
||||
modes.add(ColorMode.RGB)
|
||||
if self.room.support_brightness and not modes:
|
||||
modes.add(ColorMode.BRIGHTNESS)
|
||||
if not modes:
|
||||
modes.add(ColorMode.ONOFF)
|
||||
|
||||
return modes
|
||||
|
||||
@property
|
||||
def color_mode(self) -> str | None:
|
||||
"""Return the active color mode."""
|
||||
|
||||
if self.room.support_color_temp:
|
||||
if self.room.support_rgb and self.room.rgb['active']:
|
||||
return ColorMode.RGB
|
||||
else:
|
||||
return ColorMode.COLOR_TEMP
|
||||
if self.room.support_brightness:
|
||||
return ColorMode.BRIGHTNESS
|
||||
else:
|
||||
return ColorMode.ONOFF
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
await self.room.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.room.turn_off()
|
||||
|
||||
class CyncSwitchEntity(LightEntity):
|
||||
"""Representation of a Cync Switch Light Entity."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, cync_switch) -> None:
|
||||
"""Initialize the light."""
|
||||
self.cync_switch = cync_switch
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.cync_switch.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.cync_switch.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||
suggested_area = f"{self.cync_switch.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
return 'cync_switch_' + self.cync_switch.device_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the switch."""
|
||||
return self.cync_switch.name
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if light is on."""
|
||||
return self.cync_switch.power_state
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this switch between 0..255."""
|
||||
return round(self.cync_switch.brightness*255/100)
|
||||
|
||||
@property
|
||||
def max_color_temp_kelvin(self) -> int:
|
||||
"""Return maximum supported color temperature."""
|
||||
return self.cync_switch.max_color_temp_kelvin
|
||||
|
||||
@property
|
||||
def min_color_temp_kelvin(self) -> int:
|
||||
"""Return minimum supported color temperature."""
|
||||
return self.cync_switch.min_color_temp_kelvin
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int | None:
|
||||
"""Return the color temperature of this light for HA."""
|
||||
return self.min_color_temp_kelvin + round((self.max_color_temp_kelvin-self.min_color_temp_kelvin)*self.cync_switch.color_temp/100)
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
"""Return the RGB color tuple of this light switch"""
|
||||
return (self.cync_switch.rgb['r'],self.cync_switch.rgb['g'],self.cync_switch.rgb['b'])
|
||||
|
||||
@property
|
||||
def supported_color_modes(self) -> set[str] | None:
|
||||
"""Return list of available color modes."""
|
||||
|
||||
modes: set[ColorMode | str] = set()
|
||||
|
||||
if self.cync_switch.support_color_temp:
|
||||
modes.add(ColorMode.COLOR_TEMP)
|
||||
if self.cync_switch.support_rgb:
|
||||
modes.add(ColorMode.RGB)
|
||||
if self.cync_switch.support_brightness and not modes:
|
||||
modes.add(ColorMode.BRIGHTNESS)
|
||||
if not modes:
|
||||
modes.add(ColorMode.ONOFF)
|
||||
|
||||
return modes
|
||||
|
||||
@property
|
||||
def color_mode(self) -> str | None:
|
||||
"""Return the active color mode."""
|
||||
|
||||
if self.cync_switch.support_color_temp:
|
||||
if self.cync_switch.support_rgb and self.cync_switch.rgb['active']:
|
||||
return ColorMode.RGB
|
||||
else:
|
||||
return ColorMode.COLOR_TEMP
|
||||
if self.cync_switch.support_brightness:
|
||||
return ColorMode.BRIGHTNESS
|
||||
else:
|
||||
return ColorMode.ONOFF
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
await self.cync_switch.turn_on(kwargs.get(ATTR_RGB_COLOR),kwargs.get(ATTR_BRIGHTNESS),kwargs.get(ATTR_COLOR_TEMP_KELVIN))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.cync_switch.turn_off()
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"domain": "cync_lights",
|
||||
"name": "Cync Lights",
|
||||
"version": "1.0.1",
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/nikshriv/cync_lights",
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [],
|
||||
"codeowners": [
|
||||
"@nikshriv"
|
||||
],
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Cync User Credentials",
|
||||
"description": "Please enter your Cync account credentials"
|
||||
},
|
||||
"two_factor_code": {
|
||||
"data": {
|
||||
"two_factor_code":"Two Factor Code"
|
||||
},
|
||||
"title": "Cync Two Factor Authentication",
|
||||
"description": "Please check your email and enter the two factor code"
|
||||
},
|
||||
"select_switches": {
|
||||
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
|
||||
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
|
||||
"data":{
|
||||
"rooms":"Rooms [room (home)]",
|
||||
"subgroups":"Groups [group (room:home)]",
|
||||
"switches":"Switches [switch/bulb (room:home)]",
|
||||
"motion_sensors":"Motion Sensors [sensor (room:home)]",
|
||||
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid Cync user credentials or two factor code",
|
||||
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
|
||||
},
|
||||
"abort": {
|
||||
"reauth_successful": "Reauthorization successful"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Reload Cync Configuration",
|
||||
"description": "In case you have added new devices to your Cync account, you can opt to reauthorize and download your latest configuration.",
|
||||
"data":{
|
||||
"re-authenticate":"Reauthorize?"
|
||||
}
|
||||
},
|
||||
"two_factor_code": {
|
||||
"data": {
|
||||
"two_factor_code":"Two Factor Code"
|
||||
},
|
||||
"title": "Cync Two Factor Authentication",
|
||||
"description": "Please check your email and enter the two factor code"
|
||||
},
|
||||
"select_switches": {
|
||||
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
|
||||
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
|
||||
"data":{
|
||||
"rooms":"Rooms [room (home)]",
|
||||
"subgroups":"Groups [group (room:home)]",
|
||||
"switches":"Switches [switch/bulb (room:home)]",
|
||||
"motion_sensors":"Motion Sensors [sensor (room:home)]",
|
||||
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid Cync user credentials or two factor code",
|
||||
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Platform for light integration."""
|
||||
from __future__ import annotations
|
||||
from typing import Any
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import DOMAIN
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
hub = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
new_devices = []
|
||||
for switch_id in hub.cync_switches:
|
||||
if not hub.cync_switches[switch_id]._update_callback and hub.cync_switches[switch_id].plug and switch_id in config_entry.options["switches"]:
|
||||
new_devices.append(CyncPlugEntity(hub.cync_switches[switch_id]))
|
||||
|
||||
if new_devices:
|
||||
async_add_entities(new_devices)
|
||||
|
||||
class CyncPlugEntity(SwitchEntity):
|
||||
"""Representation of a Cync Switch Light Entity."""
|
||||
|
||||
should_poll = False
|
||||
|
||||
def __init__(self, cync_switch) -> None:
|
||||
"""Initialize the light."""
|
||||
self.cync_switch = cync_switch
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when this Entity has been added to HA."""
|
||||
self.cync_switch.register(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Entity being removed from hass."""
|
||||
self.cync_switch.reset()
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device registry information for this entity."""
|
||||
return DeviceInfo(
|
||||
identifiers = {(DOMAIN, f"{self.cync_switch.room.name} ({self.cync_switch.home_name})")},
|
||||
manufacturer = "Cync by Savant",
|
||||
name = f"{self.cync_switch.room.name} ({self.cync_switch.home_name})",
|
||||
suggested_area = f"{self.cync_switch.room.name}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return Unique ID string."""
|
||||
return 'cync_switch_' + self.cync_switch.device_id
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the switch."""
|
||||
return self.cync_switch.name
|
||||
|
||||
@property
|
||||
def device_class(self) -> str | None:
|
||||
"""Return the device class"""
|
||||
return SwitchDeviceClass.OUTLET
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if light is on."""
|
||||
return self.cync_switch.power_state
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the outlet."""
|
||||
await self.cync_switch.turn_on(None, None, None)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the outlet."""
|
||||
await self.cync_switch.turn_off()
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Cync User Credentials",
|
||||
"description": "Please enter your Cync account credentials"
|
||||
},
|
||||
"two_factor_code": {
|
||||
"data": {
|
||||
"two_factor_code":"Two Factor Code"
|
||||
},
|
||||
"title": "Cync Two Factor Authentication",
|
||||
"description": "Please check your email and enter the two factor code"
|
||||
},
|
||||
"select_switches": {
|
||||
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
|
||||
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
|
||||
"data":{
|
||||
"rooms":"Rooms [room (home)]",
|
||||
"subgroups":"Groups [group (room:home)]",
|
||||
"switches":"Switches [switch/bulb (room:home)]",
|
||||
"motion_sensors":"Motion Sensors [sensor (room:home)]",
|
||||
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid Cync user credentials or two factor code",
|
||||
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
|
||||
},
|
||||
"abort": {
|
||||
"reauth_successful": "Reauthorization successful"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Reload Cync Configuration",
|
||||
"description": "In case you have added new devices to your Cync account, you can opt to reauthorize and download your latest configuration.",
|
||||
"data":{
|
||||
"re-authenticate":"Reauthorize?"
|
||||
}
|
||||
},
|
||||
"two_factor_code": {
|
||||
"data": {
|
||||
"two_factor_code":"Two Factor Code"
|
||||
},
|
||||
"title": "Cync Two Factor Authentication",
|
||||
"description": "Please check your email and enter the two factor code"
|
||||
},
|
||||
"select_switches": {
|
||||
"title": "Select Rooms, Groups, Switches, Motion Sensors, and Ambient Light Sensors",
|
||||
"description": "Please select the rooms, groups, individual switches/bulbs, motion sensors, and ambient light sensors that you would like to add to Home Assistant",
|
||||
"data":{
|
||||
"rooms":"Rooms [room (home)]",
|
||||
"subgroups":"Groups [group (room:home)]",
|
||||
"switches":"Switches [switch/bulb (room:home)]",
|
||||
"motion_sensors":"Motion Sensors [sensor (room:home)]",
|
||||
"ambient_light_sensors":"Ambient Light Sensors [sensor (room:home)]]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid Cync user credentials or two factor code",
|
||||
"unknown": "Invalid or unsupported Cync configuration, please ensure there is at least one WiFi connected Cync device in your Home(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user