Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Settings Controls - On speed, off speed, and mode. #20

Merged
merged 5 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 109 additions & 61 deletions custom_components/ac_infinity/ac_infinity.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
from datetime import timedelta
from typing import Any, List
from typing import Any

from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.util import Throttle

from .client import ACInfinityClient
from .const import (
DEVICE_KEY_DEVICE_ID,
DEVICE_KEY_DEVICE_INFO,
DEVICE_KEY_DEVICE_NAME,
DEVICE_KEY_DEVICE_TYPE,
DEVICE_KEY_HW_VERSION,
DEVICE_KEY_MAC_ADDR,
DEVICE_KEY_PORTS,
DEVICE_KEY_SW_VERSION,
DEVICE_PORT_KEY_NAME,
DEVICE_PORT_KEY_PORT,
DOMAIN,
HOST,
MANUFACTURER,
PROPERTY_KEY_DEVICE_ID,
PROPERTY_KEY_DEVICE_INFO,
PROPERTY_KEY_DEVICE_NAME,
PROPERTY_KEY_DEVICE_TYPE,
PROPERTY_KEY_HW_VERSION,
PROPERTY_KEY_MAC_ADDR,
PROPERTY_KEY_PORTS,
PROPERTY_KEY_SW_VERSION,
PROPERTY_PORT_KEY_NAME,
PROPERTY_PORT_KEY_PORT,
)


class ACInfinityDevice:
def __init__(self, device_json) -> None:
# device info
self._device_id = str(device_json[DEVICE_KEY_DEVICE_ID])
self._mac_addr = device_json[DEVICE_KEY_MAC_ADDR]
self._device_name = device_json[DEVICE_KEY_DEVICE_NAME]

self._device_id = str(device_json[PROPERTY_KEY_DEVICE_ID])
self._mac_addr = device_json[PROPERTY_KEY_MAC_ADDR]
self._device_name = device_json[PROPERTY_KEY_DEVICE_NAME]
self._identifier = (DOMAIN, self._device_id)
self._ports = [
ACInfinityDevicePort(port)
for port in device_json[DEVICE_KEY_DEVICE_INFO][DEVICE_KEY_PORTS]
ACInfinityDevicePort(self, port)
for port in device_json[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS]
]

self._device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_id)},
identifiers={self._identifier},
name=self._device_name,
manufacturer=MANUFACTURER,
hw_version=device_json[DEVICE_KEY_HW_VERSION],
sw_version=device_json[DEVICE_KEY_SW_VERSION],
hw_version=device_json[PROPERTY_KEY_HW_VERSION],
sw_version=device_json[PROPERTY_KEY_SW_VERSION],
model=self.__get_device_model_by_device_type(
device_json[DEVICE_KEY_DEVICE_TYPE]
device_json[PROPERTY_KEY_DEVICE_TYPE]
),
)

Expand All @@ -66,18 +66,30 @@ def ports(self):
def device_info(self):
return self._device_info

@property
def identifier(self):
return self._identifier

def __get_device_model_by_device_type(self, device_type: int):
match device_type:
case 11:
return "Controller 69 Pro (CTR69P)"
return "UIS Controller 69 Pro (CTR69P)"
case _:
return f"Controller Type {device_type}"
return f"UIS Controller Type {device_type}"


class ACInfinityDevicePort:
def __init__(self, device_json) -> None:
self._port_id = device_json[DEVICE_PORT_KEY_PORT]
self._port_name = device_json[DEVICE_PORT_KEY_NAME]
def __init__(self, device: ACInfinityDevice, device_port_json) -> None:
self._port_id = device_port_json[PROPERTY_PORT_KEY_PORT]
self._port_name = device_port_json[PROPERTY_PORT_KEY_NAME]

self._device_info = DeviceInfo(
identifiers={(DOMAIN, f"{device._device_id}_{self._port_id}")},
name=f"{device._device_name} {self._port_name}",
manufacturer=MANUFACTURER,
via_device=device.identifier,
model="UIS Enabled Device",
)

@property
def port_id(self):
Expand All @@ -87,66 +99,102 @@ def port_id(self):
def port_name(self):
return self._port_name

@property
def device_info(self):
return self._device_info


class ACInfinity:
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)

def __init__(self, email: str, password: str) -> None:
self._client = ACInfinityClient(HOST, email, password)
self._data: list[dict[str, Any]] = []
self._devices: dict[str, dict[str, Any]] = {}
self._port_settings: dict[str, dict[int, Any]] = {}

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""refreshes the values of properties and settings from the AC infinity API"""
try:
if not self._client.is_logged_in():
await self._client.login()

self._data = await self._client.get_all_device_info()
device_list = await self._client.get_all_device_info()
for device in device_list:
device_id = device[PROPERTY_KEY_DEVICE_ID]
self._devices[device_id] = device
self._port_settings[device_id] = {}
for port in device[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS]:
port_id = port[PROPERTY_PORT_KEY_PORT]
self._port_settings[device_id][
port_id
] = await self._client.get_device_port_settings(device_id, port_id)

except Exception:
raise UpdateFailed from Exception

def get_all_device_meta_data(self) -> List[ACInfinityDevice]:
def get_all_device_meta_data(self) -> list[ACInfinityDevice]:
"""gets device metadata, such as ids, labels, macaddr, etc.. that are not expected to change"""
if (self._data) is None:
if (self._devices) is None:
return []

return [ACInfinityDevice(device) for device in self._data]

def get_device_property(self, device_id: (str | int), property: str):
"""gets a property, if it exists, from a given device, if it exists"""
result = next(
(
device
for device in self._data
if device[DEVICE_KEY_DEVICE_ID] == str(device_id)
),
None,
)
return [ACInfinityDevice(device) for device in self._devices.values()]

if result is not None:
if property in result:
return result[property]
elif property in result[DEVICE_KEY_DEVICE_INFO]:
return result[DEVICE_KEY_DEVICE_INFO][property]
def get_device_property(self, device_id: (str | int), property_key: str):
"""gets a property of a controller, if it exists, from a given device, if it exists"""
if str(device_id) in self._devices:
result = self._devices[str(device_id)]
if property_key in result:
return result[property_key]
elif property_key in result[PROPERTY_KEY_DEVICE_INFO]:
return result[PROPERTY_KEY_DEVICE_INFO][property_key]

return None

def get_device_port_property(
self, device_id: (str | int), port_id: int, property: str
self, device_id: (str | int), port_id: int, property_key: str
):
"""gets a property, if it exists, from the given port, if it exists, from the given device, if it exists"""
result = next(
(
port
for device in self._data
if device[DEVICE_KEY_DEVICE_ID] == str(device_id)
for port in device[DEVICE_KEY_DEVICE_INFO][DEVICE_KEY_PORTS]
if port[DEVICE_PORT_KEY_PORT] == port_id
),
None,
)
"""gets a property, if it exists, from the given port, if it exists, from a child of the given controller device, if it exists

Properties are read-only values reported from the parent controller via devInfoListAll, as opposed to settings with are read/write
values reported from getdevModeSettingList for the individual port device
"""
if str(device_id) in self._devices:
device = self._devices[str(device_id)]
result = next(
(
port
for port in device[PROPERTY_KEY_DEVICE_INFO][PROPERTY_KEY_PORTS]
if port[PROPERTY_PORT_KEY_PORT] == port_id
),
None,
)

if result is not None and property_key in result:
return result[property_key]

if result is not None and property in result:
return result[property]
return None

def get_device_port_setting(
self, device_id: (str | int), port_id: int, setting: str
):
"""gets the current set value for a given device setting

Settings are read/write values reported from getdevModeSettinList for an individual port device, as opposed to
port properties, which are read-only values reported by the parent controller via devInfoListAll
"""
device_id_str = str(device_id)
if (
device_id_str in self._port_settings
and port_id in self._port_settings[device_id_str]
and setting in self._port_settings[device_id_str][port_id]
):
return self._port_settings[device_id_str][port_id][setting]

return None

async def set_device_port_setting(
self, device_id: (str | int), port_id: int, setting: str, value: int
):
"""set a desired value for a given device setting"""
await self._client.set_device_port_setting(device_id, port_id, setting, value)
6 changes: 3 additions & 3 deletions custom_components/ac_infinity/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from custom_components.ac_infinity.const import DEVICE_PORT_KEY_ONLINE, DOMAIN
from custom_components.ac_infinity.const import DOMAIN, SENSOR_PORT_KEY_ONLINE

from .ac_infinity import ACInfinity, ACInfinityDevice, ACInfinityDevicePort
from .utilities import get_device_port_property_name, get_device_port_property_unique_id
Expand All @@ -29,7 +29,7 @@ def __init__(
self._property_key = property_key

self._attr_icon = icon
self._attr_device_info = device.device_info
self._attr_device_info = port.device_info
self._attr_device_class = device_class
self._attr_unique_id = get_device_port_property_unique_id(
device, port, property_key
Expand All @@ -51,7 +51,7 @@ async def async_setup_entry(
acis: ACInfinity = hass.data[DOMAIN][config.entry_id]

device_sensors = {
DEVICE_PORT_KEY_ONLINE: {
SENSOR_PORT_KEY_ONLINE: {
"label": "Status",
"deviceClass": BinarySensorDeviceClass.PLUG,
"icon": "mdi:power",
Expand Down
75 changes: 73 additions & 2 deletions custom_components/ac_infinity/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

API_URL_LOGIN = "/api/user/appUserLogin"
API_URL_GET_DEVICE_INFO_LIST_ALL = "/api/user/devInfoListAll"
API_URL_GET_DEV_MODE_SETTING = "/api/dev/getdevModeSettingList"
API_URL_ADD_DEV_MODE = "/api/dev/addDevMode"


class ACInfinityClient:
Expand All @@ -28,14 +30,76 @@ def is_logged_in(self):

async def get_all_device_info(self):
if not self.is_logged_in():
raise ACInfinityClientCannotConnect("Aerogarden client is not logged in.")
raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.")

headers = self.__create_headers(use_auth_token=True)
json = await self.__post(
API_URL_GET_DEVICE_INFO_LIST_ALL, {"userId": self._user_id}, headers
)
return json["data"]

async def get_device_port_settings(self, device_id: (str | int), port_id: int):
if not self.is_logged_in():
raise ACInfinityClientCannotConnect("AC Infinity client is not logged in.")

headers = self.__create_headers(use_auth_token=True)
json = await self.__post(
API_URL_GET_DEV_MODE_SETTING, {"devId": device_id, "port": port_id}, headers
)
return json["data"]

async def set_device_port_setting(
self, device_id: (str | int), port_id: int, setting: str, value: int
):
active_settings = await self.get_device_port_settings(device_id, port_id)
payload = {
"acitveTimerOff": active_settings["acitveTimerOff"],
"acitveTimerOn": active_settings["acitveTimerOn"],
"activeCycleOff": active_settings["activeCycleOff"],
"activeCycleOn": active_settings["activeCycleOn"],
"activeHh": active_settings["activeHh"],
"activeHt": active_settings["activeHt"],
"activeHtVpd": active_settings["activeHtVpd"],
"activeHtVpdNums": active_settings["activeHtVpdNums"],
"activeLh": active_settings["activeLh"],
"activeLt": active_settings["activeLt"],
"activeLtVpd": active_settings["activeLtVpd"],
"activeLtVpdNums": active_settings["activeLtVpdNums"],
"atType": active_settings["atType"],
"devHh": active_settings["devHh"],
"devHt": active_settings["devHt"],
"devHtf": active_settings["devHtf"],
"devId": active_settings["devId"],
"devLh": active_settings["devLh"],
"devLt": active_settings["devLt"],
"devLtf": active_settings["devLtf"],
"externalPort": active_settings["externalPort"],
"hTrend": active_settings["hTrend"],
"isOpenAutomation": active_settings["isOpenAutomation"],
"onSpead": active_settings["onSpead"],
"offSpead": active_settings["offSpead"],
"onlyUpdateSpeed": active_settings["onlyUpdateSpeed"],
"schedEndtTime": active_settings["schedEndtTime"],
"schedStartTime": active_settings["schedStartTime"],
"settingMode": active_settings["settingMode"],
"surplus": active_settings["surplus"] or 0,
"tTrend": active_settings["tTrend"],
"targetHumi": active_settings["targetHumi"],
"targetHumiSwitch": active_settings["targetHumiSwitch"],
"targetTSwitch": active_settings["targetTSwitch"],
"targetTemp": active_settings["targetTemp"],
"targetTempF": active_settings["targetTempF"],
"targetVpd": active_settings["targetVpd"],
"targetVpdSwitch": active_settings["targetVpdSwitch"],
"trend": active_settings["trend"],
"unit": active_settings["unit"],
"vpdSettingMode": active_settings["vpdSettingMode"],
}

payload[setting] = int(value)
headers = self.__create_headers(use_auth_token=True)
_ = await self.__post(API_URL_ADD_DEV_MODE, payload, headers)

async def __post(self, path, post_data, headers):
async with async_timeout.timeout(10), aiohttp.ClientSession(
raise_for_status=False, headers=headers
Expand All @@ -45,7 +109,10 @@ async def __post(self, path, post_data, headers):

json = await response.json()
if json["code"] != 200:
raise ACInfinityClientInvalidAuth
if path == API_URL_LOGIN:
raise ACInfinityClientInvalidAuth
else:
raise ACInfinityClientRequestFailed(json)

return json

Expand All @@ -67,3 +134,7 @@ class ACInfinityClientCannotConnect(HomeAssistantError):

class ACInfinityClientInvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""


class ACInfinityClientRequestFailed(HomeAssistantError):
"""Error to indicate a request failed"""
Loading