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
|
||||
@@ -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(str(type(e).__name__) + ": " + str(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(str(type(e).__name__) + ": " + str(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(str(type(e).__name__) + ": " + str(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(str(type(e).__name__) + ": " + str(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,870 @@
|
||||
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,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],
|
||||
"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,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],
|
||||
"COLORTEMP":[5,6,7,8,10,11,14,15,19,20,21,22,23,25,26,28,29,30,31,32,33,34,35,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],
|
||||
"RGB":[6,7,8,21,22,23,30,31,32,33,34,35,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],
|
||||
"MOTION":[37,49,54],
|
||||
"AMBIENT_LIGHT":[37,49,54],
|
||||
"WIFICONTROL":[36,37,38,39,40,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],
|
||||
"PLUG":[64,65,66,67,68],
|
||||
"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(str(type(e).__name__) + ": " + str(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(str(type(e).__name__) + ": " + str(e))
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
if not self.shutting_down:
|
||||
_LOGGER.error("Connection to Cync server reset, restarting in 15 seconds")
|
||||
await asyncio.sleep(15)
|
||||
else:
|
||||
_LOGGER.debug("Cync client shutting down")
|
||||
except Exception as e:
|
||||
_LOGGER.error(str(type(e).__name__) + ": " + str(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_kelvin,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_kelvin,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_kelvin,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_kelvin,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(str(type(e).__name__) + ": " + str(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_kelvin = 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)
|
||||
# Sync wants the color temp as a percentage of the range. So we need to
|
||||
# calculate what percentage of the color temp range is being requested
|
||||
# before sending it to the server.
|
||||
color_temp = round(
|
||||
(
|
||||
(attr_ct - self.min_color_temp_kelvin) /
|
||||
self.max_color_temp_kelvin
|
||||
) * 100
|
||||
)
|
||||
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_kelvin
|
||||
_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_kelvin for device_id in self.switches_support_color_temp] + [self.hub.cync_rooms[room_id].color_temp_kelvin 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_kelvin or _rgb != self.rgb:
|
||||
self.power_state = _power_state
|
||||
self.brightness = _brightness
|
||||
self.color_temp_kelvin = _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_kelvin = 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:
|
||||
# Sync wants the color temp as a percentage of the range. So we need to
|
||||
# calculate what percentage of the color temp range is being requested
|
||||
# before sending it to the server.
|
||||
color_temp = round(
|
||||
(
|
||||
(attr_ct - self.min_color_temp_kelvin) /
|
||||
self.max_color_temp_kelvin
|
||||
) * 100
|
||||
)
|
||||
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
|
||||
# Cync sends the color temp as a percentage from 0-100 based on the max and min
|
||||
# color temp. Most Cync bulbs support 2000K-7000K, so we have to calculate what
|
||||
# is actually being requested.
|
||||
_color_temp = round(
|
||||
(self.max_color_temp_kelvin - self.min_color_temp_kelvin) *
|
||||
(color_temp / 100) +
|
||||
self.min_color_temp_kelvin
|
||||
)
|
||||
if self.power_state != state or self.brightness != brightness or self.color_temp_kelvin != _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
|
||||
# Cync sends the color temp as a percentage from 0-100 based on the max and
|
||||
# min color temp. Most Cync bulbs support 2000K-7000K, so we have to
|
||||
# calculate what is actually being requested.
|
||||
self.color_temp_kelvin = _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,104 @@
|
||||
"""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
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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,253 @@
|
||||
"""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
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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.room.color_temp_kelvin
|
||||
|
||||
@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:
|
||||
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.cync_switch.color_temp_kelvin
|
||||
|
||||
@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:
|
||||
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,82 @@
|
||||
"""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
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user