mirror of
https://github.com/no2chem/wideq.git
synced 2025-05-16 15:20:09 -07:00
Initial thinq2 support, works with LA090HSV5 ac
This commit is contained in:
parent
267a880340
commit
8617831453
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,3 +6,5 @@ __pycache__
|
|||||||
.vscode/
|
.vscode/
|
||||||
# Ignore Mac system files
|
# Ignore Mac system files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
# Ignore devcontainer for now (should this go in source control?)
|
||||||
|
.devcontainer
|
21
example.py
21
example.py
@ -9,6 +9,7 @@ import re
|
|||||||
import os.path
|
import os.path
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
STATE_FILE = "wideq_state.json"
|
STATE_FILE = "wideq_state.json"
|
||||||
LOGGER = logging.getLogger("wideq.example")
|
LOGGER = logging.getLogger("wideq.example")
|
||||||
@ -34,6 +35,13 @@ def ls(client):
|
|||||||
print("{0.id}: {0.name} ({0.type.name} {0.model_id})".format(device))
|
print("{0.id}: {0.name} ({0.type.name} {0.model_id})".format(device))
|
||||||
|
|
||||||
|
|
||||||
|
def info(client, device_id):
|
||||||
|
"""Dump info on a device."""
|
||||||
|
|
||||||
|
device = client.get_device(device_id)
|
||||||
|
pprint(vars(device), indent=4, width=1)
|
||||||
|
|
||||||
|
|
||||||
def gen_mon(client, device_id):
|
def gen_mon(client, device_id):
|
||||||
"""Monitor any other device but AC device,
|
"""Monitor any other device but AC device,
|
||||||
displaying generic information about its status.
|
displaying generic information about its status.
|
||||||
@ -51,8 +59,10 @@ def gen_mon(client, device_id):
|
|||||||
if data:
|
if data:
|
||||||
try:
|
try:
|
||||||
res = model.decode_monitor(data)
|
res = model.decode_monitor(data)
|
||||||
|
print(res)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("status data: {!r}".format(data))
|
print("status data: {!r}".format(data))
|
||||||
|
"""
|
||||||
else:
|
else:
|
||||||
for key, value in res.items():
|
for key, value in res.items():
|
||||||
try:
|
try:
|
||||||
@ -66,13 +76,9 @@ def gen_mon(client, device_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
elif isinstance(desc, wideq.RangeValue):
|
elif isinstance(desc, wideq.RangeValue):
|
||||||
print(
|
print('- {0}: {1} ({2.min}-{2.max})'.format(
|
||||||
"- {0}: {1} ({2.min}-{2.max})".format(
|
key, value, desc,
|
||||||
key,
|
)) """
|
||||||
value,
|
|
||||||
desc,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
@ -204,6 +210,7 @@ EXAMPLE_COMMANDS = {
|
|||||||
"set-temp-freezer": set_temp_freezer,
|
"set-temp-freezer": set_temp_freezer,
|
||||||
"turn": turn,
|
"turn": turn,
|
||||||
"ac-config": ac_config,
|
"ac-config": ac_config,
|
||||||
|
"info": info,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
109
wideq/ac.py
109
wideq/ac.py
@ -155,6 +155,7 @@ class ACOp(enum.Enum):
|
|||||||
"""Whether a device is on or off."""
|
"""Whether a device is on or off."""
|
||||||
|
|
||||||
OFF = "@AC_MAIN_OPERATION_OFF_W"
|
OFF = "@AC_MAIN_OPERATION_OFF_W"
|
||||||
|
ON = "@AC_MAIN_OPERATION_ON_W" # (single) on
|
||||||
RIGHT_ON = "@AC_MAIN_OPERATION_RIGHT_ON_W" # Right fan only.
|
RIGHT_ON = "@AC_MAIN_OPERATION_RIGHT_ON_W" # Right fan only.
|
||||||
LEFT_ON = "@AC_MAIN_OPERATION_LEFT_ON_W" # Left fan only.
|
LEFT_ON = "@AC_MAIN_OPERATION_LEFT_ON_W" # Left fan only.
|
||||||
ALL_ON = "@AC_MAIN_OPERATION_ALL_ON_W" # Both fans (or only fan) on.
|
ALL_ON = "@AC_MAIN_OPERATION_ALL_ON_W" # Both fans (or only fan) on.
|
||||||
@ -202,7 +203,7 @@ class ACDevice(Device):
|
|||||||
def supported_operations(self):
|
def supported_operations(self):
|
||||||
"""Get a list of the ACOp Operations the device supports."""
|
"""Get a list of the ACOp Operations the device supports."""
|
||||||
|
|
||||||
mapping = self.model.value("Operation").options
|
mapping = self.model.value("airState.operation").options
|
||||||
return [ACOp(o) for i, o in mapping.items()]
|
return [ACOp(o) for i, o in mapping.items()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -239,7 +240,7 @@ class ACDevice(Device):
|
|||||||
def set_celsius(self, c):
|
def set_celsius(self, c):
|
||||||
"""Set the device's target temperature in Celsius degrees."""
|
"""Set the device's target temperature in Celsius degrees."""
|
||||||
|
|
||||||
self._set_control("TempCfg", c)
|
self._set_control("airState.tempState.target", c)
|
||||||
|
|
||||||
def set_fahrenheit(self, f):
|
def set_fahrenheit(self, f):
|
||||||
"""Set the device's target temperature in Fahrenheit degrees."""
|
"""Set the device's target temperature in Fahrenheit degrees."""
|
||||||
@ -276,86 +277,80 @@ class ACDevice(Device):
|
|||||||
`set_zones`.
|
`set_zones`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._get_config("DuctZone")
|
# don't have api data for v2 zones, not sure about format
|
||||||
|
return []
|
||||||
|
|
||||||
def set_jet_mode(self, jet_opt):
|
def set_jet_mode(self, jet_opt):
|
||||||
"""Set jet mode to a value from the `ACJetMode` enum."""
|
"""Set jet mode to a value from the `ACJetMode` enum."""
|
||||||
|
|
||||||
jet_opt_value = self.model.enum_value("Jet", jet_opt.value)
|
jet_opt_value = self.model.enum_value(
|
||||||
self._set_control("Jet", jet_opt_value)
|
"airState.wMode.jet", jet_opt.value
|
||||||
|
)
|
||||||
|
self._set_control("airState.wMode.jet", jet_opt_value)
|
||||||
|
|
||||||
def set_fan_speed(self, speed):
|
def set_fan_speed(self, speed):
|
||||||
"""Set the fan speed to a value from the `ACFanSpeed` enum."""
|
"""Set the fan speed to a value from the `ACFanSpeed` enum."""
|
||||||
|
|
||||||
speed_value = self.model.enum_value("WindStrength", speed.value)
|
speed_value = self.model.enum_value(
|
||||||
self._set_control("WindStrength", speed_value)
|
"airState.windStrength", speed.value
|
||||||
|
)
|
||||||
|
self._set_control("airState.windStrength", speed_value)
|
||||||
|
|
||||||
def set_horz_swing(self, swing):
|
def set_horz_swing(self, swing):
|
||||||
"""Set the horizontal swing to a value from the `ACHSwingMode` enum."""
|
"""Set the horizontal swing to a value from the `ACHSwingMode` enum."""
|
||||||
|
|
||||||
swing_value = self.model.enum_value("WDirHStep", swing.value)
|
swing_value = self.model.enum_value("airState.wDir.hStep", swing.value)
|
||||||
self._set_control("WDirHStep", swing_value)
|
self._set_control("airState.wDir.hStep", swing_value)
|
||||||
|
|
||||||
def set_vert_swing(self, swing):
|
def set_vert_swing(self, swing):
|
||||||
"""Set the vertical swing to a value from the `ACVSwingMode` enum."""
|
"""Set the vertical swing to a value from the `ACVSwingMode` enum."""
|
||||||
|
|
||||||
swing_value = self.model.enum_value("WDirVStep", swing.value)
|
swing_value = self.model.enum_value("airState.wDir.vStep", swing.value)
|
||||||
self._set_control("WDirVStep", swing_value)
|
self._set_control("airState.wDir.vStep", swing_value)
|
||||||
|
|
||||||
def set_mode(self, mode):
|
def set_mode(self, mode):
|
||||||
"""Set the device's operating mode to an `OpMode` value."""
|
"""Set the device's operating mode to an `OpMode` value."""
|
||||||
|
|
||||||
mode_value = self.model.enum_value("OpMode", mode.value)
|
mode_value = self.model.enum_value("airState.opMode", mode.value)
|
||||||
self._set_control("OpMode", mode_value)
|
self._set_control("airState.opMode", mode_value)
|
||||||
|
|
||||||
def set_on(self, is_on):
|
def set_on(self, is_on):
|
||||||
"""Turn on or off the device (according to a boolean)."""
|
"""Turn on or off the device (according to a boolean)."""
|
||||||
|
|
||||||
op = self.supported_on_operation if is_on else ACOp.OFF
|
op = self.supported_on_operation if is_on else ACOp.OFF
|
||||||
op_value = self.model.enum_value("Operation", op.value)
|
op_value = self.model.enum_value("airState.operation", op.value)
|
||||||
self._set_control("Operation", op_value)
|
self._set_control("airState.operation", op_value, command="Operation")
|
||||||
|
|
||||||
def get_filter_state(self):
|
def get_filter_state(self):
|
||||||
"""Get information about the filter."""
|
"""Get information about the filter."""
|
||||||
|
|
||||||
return self._get_config("Filter")
|
return self.get_status().filter_state
|
||||||
|
|
||||||
def get_mfilter_state(self):
|
def get_mfilter_state(self):
|
||||||
"""Get information about the "MFilter" (not sure what this is)."""
|
"""Get information about the "MFilter" (not sure what this is)."""
|
||||||
|
|
||||||
return self._get_config("MFilter")
|
return self.get_status().filter_state_max_time
|
||||||
|
|
||||||
def get_energy_target(self):
|
def get_energy_target(self):
|
||||||
"""Get the configured energy target data."""
|
"""Get the configured energy target data."""
|
||||||
|
|
||||||
return self._get_config("EnergyDesiredValue")
|
return self.get_status().energy_on_current
|
||||||
|
|
||||||
def get_outdoor_power(self):
|
def get_outdoor_power(self):
|
||||||
"""Get instant power usage in watts of the outdoor unit"""
|
"""Get instant power usage in watts of the outdoor unit"""
|
||||||
|
|
||||||
try:
|
return self.get_status().energy_on_current
|
||||||
value = self._get_config("OutTotalInstantPower")
|
|
||||||
return value["OutTotalInstantPower"]
|
|
||||||
except InvalidRequestError:
|
|
||||||
# Device does not support outdoor unit instant power usage
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_power(self):
|
def get_power(self):
|
||||||
"""Get the instant power usage in watts of the whole unit"""
|
"""Get the instant power usage in watts of the whole unit"""
|
||||||
|
|
||||||
try:
|
return self.get_status().energy_on_current
|
||||||
value = self._get_config("InOutInstantPower")
|
|
||||||
return value["InOutInstantPower"]
|
|
||||||
except InvalidRequestError:
|
|
||||||
# Device does not support whole unit instant power usage
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_light(self):
|
def get_light(self):
|
||||||
"""Get a Boolean indicating whether the display light is on."""
|
"""Get a Boolean indicating whether the display light is on."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = self._get_control("DisplayControl")
|
return self.get_status().light
|
||||||
return value == "0" # Seems backwards, but isn't.
|
|
||||||
except FailedRequestError:
|
except FailedRequestError:
|
||||||
# Device does not support reporting display light status.
|
# Device does not support reporting display light status.
|
||||||
# Since it's probably not changeable the it must be on.
|
# Since it's probably not changeable the it must be on.
|
||||||
@ -363,13 +358,15 @@ class ACDevice(Device):
|
|||||||
|
|
||||||
def get_volume(self):
|
def get_volume(self):
|
||||||
"""Get the speaker volume level."""
|
"""Get the speaker volume level."""
|
||||||
|
|
||||||
try:
|
|
||||||
value = self._get_control("SpkVolume")
|
|
||||||
return int(value)
|
|
||||||
except FailedRequestError:
|
|
||||||
return 0 # Device does not support volume control.
|
return 0 # Device does not support volume control.
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""Get status information
|
||||||
|
This method retrieves the entire device snapshot....
|
||||||
|
"""
|
||||||
|
res = self._get_deviceinfo_from_snapshot()
|
||||||
|
return ACStatus(self, res)
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
"""Poll the device's current state.
|
"""Poll the device's current state.
|
||||||
|
|
||||||
@ -413,7 +410,7 @@ class ACStatus(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cur_c(self):
|
def temp_cur_c(self):
|
||||||
return self._str_to_num(self.data["TempCur"])
|
return self._str_to_num(self.data["airState.tempState.current"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cur_f(self):
|
def temp_cur_f(self):
|
||||||
@ -421,7 +418,7 @@ class ACStatus(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cfg_c(self):
|
def temp_cfg_c(self):
|
||||||
return self._str_to_num(self.data["TempCfg"])
|
return self._str_to_num(self.data["airState.tempState.target"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cfg_f(self):
|
def temp_cfg_f(self):
|
||||||
@ -429,23 +426,47 @@ class ACStatus(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mode(self):
|
def mode(self):
|
||||||
return ACMode(lookup_enum("OpMode", self.data, self.ac))
|
return ACMode(lookup_enum("airState.opMode", self.data, self.ac))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_speed(self):
|
def fan_speed(self):
|
||||||
return ACFanSpeed(lookup_enum("WindStrength", self.data, self.ac))
|
return ACFanSpeed(
|
||||||
|
lookup_enum("airState.windStrength", self.data, self.ac)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def horz_swing(self):
|
def horz_swing(self):
|
||||||
return ACHSwingMode(lookup_enum("WDirHStep", self.data, self.ac))
|
return ACHSwingMode(
|
||||||
|
lookup_enum("airState.wDir.hStep", self.data, self.ac)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vert_swing(self):
|
def vert_swing(self):
|
||||||
return ACVSwingMode(lookup_enum("WDirVStep", self.data, self.ac))
|
return ACVSwingMode(
|
||||||
|
lookup_enum("airState.wDir.vStep", self.data, self.ac)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_state(self):
|
||||||
|
return self._str_to_num(self.data["airState.filterMngStates.useTime"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_state_max_time(self):
|
||||||
|
return self._str_to_num(self.data["airState.filterMngStates.maxTime"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def energy_on_current(self):
|
||||||
|
return self._str_to_num(self.data["airState.energy.onCurrent"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def light(self):
|
||||||
|
return self._str_to_num(
|
||||||
|
self.data["airState.lightingState.displayControl"]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
op = ACOp(lookup_enum("Operation", self.data, self.ac))
|
op = ACOp(lookup_enum("airState.operation", self.data, self.ac))
|
||||||
return op != ACOp.OFF
|
return op != ACOp.OFF
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
120
wideq/client.py
120
wideq/client.py
@ -1,6 +1,7 @@
|
|||||||
"""A high-level, convenient abstraction for interacting with the LG
|
"""A high-level, convenient abstraction for interacting with the LG
|
||||||
SmartThinQ API for most use cases.
|
SmartThinQ API for most use cases.
|
||||||
"""
|
"""
|
||||||
|
from io import UnsupportedOperation
|
||||||
import json
|
import json
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
@ -31,23 +32,29 @@ class Monitor(object):
|
|||||||
self.device_id = device_id
|
self.device_id = device_id
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.work_id = self.session.monitor_start(self.device_id)
|
"""Nothing to do for v2"""
|
||||||
|
# self.work_id = self.session.monitor_start(self.device_id)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.session.monitor_stop(self.device_id, self.work_id)
|
"""Nothing to do for v2"""
|
||||||
|
# self.session.monitor_stop(self.device_id, self.work_id)
|
||||||
|
|
||||||
def poll(self) -> Optional[bytes]:
|
def poll(self) -> Dict[str, Any]:
|
||||||
"""Get the current status data (a bytestring) or None if the
|
"""Get the current status data (a bytestring) or None if the
|
||||||
device is not yet ready.
|
device is not yet ready.
|
||||||
"""
|
"""
|
||||||
|
# return self.session.monitor_poll(self.device_id, self.work_id)
|
||||||
|
# in v2, the data is available only in the snapshot,
|
||||||
|
# getting better info without querying all devices seems to require
|
||||||
|
# mqtt
|
||||||
|
devices = self.session.get_devices()
|
||||||
|
for device in (DeviceInfo(d) for d in devices):
|
||||||
|
if device.id == self.device_id:
|
||||||
|
pollDevice = device
|
||||||
|
if pollDevice is None:
|
||||||
|
raise core.DeviceNotFoundError()
|
||||||
|
|
||||||
try:
|
return pollDevice.data["snapshot"]
|
||||||
return self.session.monitor_poll(self.device_id, self.work_id)
|
|
||||||
except core.MonitorError:
|
|
||||||
# Try to restart the task.
|
|
||||||
self.stop()
|
|
||||||
self.start()
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_json(data: bytes) -> Dict[str, Any]:
|
def decode_json(data: bytes) -> Dict[str, Any]:
|
||||||
@ -60,8 +67,7 @@ class Monitor(object):
|
|||||||
decoded status result (or None if status is not available).
|
decoded status result (or None if status is not available).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = self.poll()
|
return self.poll()
|
||||||
return self.decode_json(data) if data else None
|
|
||||||
|
|
||||||
def __enter__(self) -> "Monitor":
|
def __enter__(self) -> "Monitor":
|
||||||
self.start()
|
self.start()
|
||||||
@ -172,7 +178,11 @@ class Client(object):
|
|||||||
if "auth" in state:
|
if "auth" in state:
|
||||||
data = state["auth"]
|
data = state["auth"]
|
||||||
client._auth = core.Auth(
|
client._auth = core.Auth(
|
||||||
client.gateway, data["access_token"], data["refresh_token"]
|
client.gateway,
|
||||||
|
data["access_token"],
|
||||||
|
data["refresh_token"],
|
||||||
|
data["user_number"],
|
||||||
|
data["oauth_root"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if "session" in state:
|
if "session" in state:
|
||||||
@ -225,13 +235,9 @@ class Client(object):
|
|||||||
to reload the gateway servers and restart the session.
|
to reload the gateway servers and restart the session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
client = cls(
|
# This operation is no longer supported in v2, the
|
||||||
country=country or core.DEFAULT_COUNTRY,
|
# user no is required.
|
||||||
language=language or core.DEFAULT_LANGUAGE,
|
raise UnsupportedOperation("User number required in v2")
|
||||||
)
|
|
||||||
client._auth = core.Auth(client.gateway, None, refresh_token)
|
|
||||||
client.refresh()
|
|
||||||
return client
|
|
||||||
|
|
||||||
def model_info(self, device: "DeviceInfo") -> "ModelInfo":
|
def model_info(self, device: "DeviceInfo") -> "ModelInfo":
|
||||||
"""For a DeviceInfo object, get a ModelInfo object describing
|
"""For a DeviceInfo object, get a ModelInfo object describing
|
||||||
@ -284,7 +290,7 @@ class DeviceInfo(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def model_id(self) -> str:
|
def model_id(self) -> str:
|
||||||
return self.data["modelNm"]
|
return self.data["modelName"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
@ -292,7 +298,7 @@ class DeviceInfo(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def model_info_url(self) -> str:
|
def model_info_url(self) -> str:
|
||||||
return self.data["modelJsonUrl"]
|
return self.data["modelJsonUri"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -306,7 +312,7 @@ class DeviceInfo(object):
|
|||||||
|
|
||||||
def load_model_info(self):
|
def load_model_info(self):
|
||||||
"""Load JSON data describing the model's capabilities."""
|
"""Load JSON data describing the model's capabilities."""
|
||||||
return requests.get(self.model_info_url).json()
|
return requests.get(self.model_info_url, verify=False).json()
|
||||||
|
|
||||||
|
|
||||||
BitValue = namedtuple("BitValue", ["options"])
|
BitValue = namedtuple("BitValue", ["options"])
|
||||||
@ -333,21 +339,21 @@ class ModelInfo(object):
|
|||||||
:raises ValueError: If an unsupported type is encountered.
|
:raises ValueError: If an unsupported type is encountered.
|
||||||
"""
|
"""
|
||||||
d = self.data["Value"][name]
|
d = self.data["Value"][name]
|
||||||
if d["type"] in ("Enum", "enum"):
|
if d["data_type"] in ("Enum", "enum"):
|
||||||
return EnumValue(d["option"])
|
return EnumValue(d["value_mapping"])
|
||||||
elif d["type"] == "Range":
|
elif d["data_type"] == "Range":
|
||||||
return RangeValue(
|
return RangeValue(
|
||||||
d["option"]["min"],
|
d["option"]["min"],
|
||||||
d["option"]["max"],
|
d["option"]["max"],
|
||||||
d["option"].get("step", 1),
|
d["option"].get("step", 1),
|
||||||
)
|
)
|
||||||
elif d["type"].lower() == "bit":
|
elif d["data_type"].lower() == "bit":
|
||||||
bit_values = {opt["startbit"]: opt["value"] for opt in d["option"]}
|
bit_values = {opt["startbit"]: opt["value"] for opt in d["option"]}
|
||||||
return BitValue(bit_values)
|
return BitValue(bit_values)
|
||||||
elif d["type"].lower() == "reference":
|
elif d["data_type"].lower() == "reference":
|
||||||
ref = d["option"][0]
|
ref = d["option"][0]
|
||||||
return ReferenceValue(self.data[ref])
|
return ReferenceValue(self.data[ref])
|
||||||
elif d["type"].lower() == "string":
|
elif d["data_type"].lower() == "string":
|
||||||
return StringValue(d.get("_comment", ""))
|
return StringValue(d.get("_comment", ""))
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -368,17 +374,17 @@ class ModelInfo(object):
|
|||||||
def enum_name(self, key, value):
|
def enum_name(self, key, value):
|
||||||
"""Look up the friendly enum name for an encoded value."""
|
"""Look up the friendly enum name for an encoded value."""
|
||||||
options = self.value(key).options
|
options = self.value(key).options
|
||||||
if value not in options:
|
if str(int(value)) not in options:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Value `%s` for key `%s` not in options: %s. Values from API: "
|
"Value `%s` for key `%s` not in options: %s. Values from API: "
|
||||||
"%s",
|
"%s",
|
||||||
value,
|
str(int(value)),
|
||||||
key,
|
key,
|
||||||
options,
|
options,
|
||||||
self.data["Value"][key]["option"],
|
self.data["Value"][key]["value_mapping"],
|
||||||
)
|
)
|
||||||
return _UNKNOWN
|
return _UNKNOWN
|
||||||
return options[value]
|
return options[str(int(value))]
|
||||||
|
|
||||||
def reference_name(self, key: str, value: Any) -> Optional[str]:
|
def reference_name(self, key: str, value: Any) -> Optional[str]:
|
||||||
"""Look up the friendly name for an encoded reference value.
|
"""Look up the friendly name for an encoded reference value.
|
||||||
@ -418,10 +424,11 @@ class ModelInfo(object):
|
|||||||
|
|
||||||
def decode_monitor(self, data):
|
def decode_monitor(self, data):
|
||||||
"""Decode status data."""
|
"""Decode status data."""
|
||||||
if self.binary_monitor_data:
|
return data
|
||||||
return self.decode_monitor_binary(data)
|
# if self.binary_monitor_data:
|
||||||
else:
|
# return self.decode_monitor_binary(data)
|
||||||
return self.decode_monitor_json(data)
|
# else:
|
||||||
|
# return self.decode_monitor_json(data)
|
||||||
|
|
||||||
|
|
||||||
class Device(object):
|
class Device(object):
|
||||||
@ -440,21 +447,36 @@ class Device(object):
|
|||||||
self.device = device
|
self.device = device
|
||||||
self.model: ModelInfo = client.model_info(device)
|
self.model: ModelInfo = client.model_info(device)
|
||||||
|
|
||||||
def _set_control(self, key, value):
|
def _get_deviceinfo_from_snapshot(self):
|
||||||
|
# probably should cache the snapshot somehow
|
||||||
|
devices = self.client.session.get_devices()
|
||||||
|
for device in (DeviceInfo(d) for d in devices):
|
||||||
|
if device.id == self.device.id:
|
||||||
|
pollDevice = device
|
||||||
|
if pollDevice is None:
|
||||||
|
raise core.DeviceNotFoundError()
|
||||||
|
return pollDevice.data["snapshot"]
|
||||||
|
|
||||||
|
def _set_control(self, key, value, command="Set", ctrlKey="basicCtrl"):
|
||||||
"""Set a device's control for `key` to `value`."""
|
"""Set a device's control for `key` to `value`."""
|
||||||
self.client.session.set_device_controls(
|
self.client.session.device_control(
|
||||||
self.device.id,
|
self.device.id,
|
||||||
{key: value},
|
{
|
||||||
|
"ctrlKey": ctrlKey,
|
||||||
|
"command": command,
|
||||||
|
"dataKey": key,
|
||||||
|
"dataValue": value,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_config(self, key):
|
def _get_config(self, key, ctrlKey="basicCtrl"):
|
||||||
"""Look up a device's configuration for a given value.
|
"""Look up a device's configuration for a given value.
|
||||||
|
|
||||||
The response is parsed as base64-encoded JSON.
|
The response is parsed as base64-encoded JSON.
|
||||||
"""
|
"""
|
||||||
data = self.client.session.get_device_config(
|
data = self.client.session.device_control(
|
||||||
self.device.id,
|
self.device.id,
|
||||||
key,
|
{"ctrlKey": ctrlKey, "command": "Get", "dataKey": key},
|
||||||
)
|
)
|
||||||
data = base64.b64decode(data).decode("utf8")
|
data = base64.b64decode(data).decode("utf8")
|
||||||
try:
|
try:
|
||||||
@ -470,15 +492,9 @@ class Device(object):
|
|||||||
|
|
||||||
def _get_control(self, key):
|
def _get_control(self, key):
|
||||||
"""Look up a device's control value."""
|
"""Look up a device's control value."""
|
||||||
data = self.client.session.get_device_config(
|
# unsupported api in v2- use snapshot or mqtt
|
||||||
self.device.id,
|
# currently just returns empty value
|
||||||
key,
|
return ""
|
||||||
"Control",
|
|
||||||
)
|
|
||||||
|
|
||||||
# The response comes in a funky key/value format: "(key:value)".
|
|
||||||
_, value = data[1:-1].split(":")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def monitor_start(self):
|
def monitor_start(self):
|
||||||
"""Start monitoring the device's status."""
|
"""Start monitoring the device's status."""
|
||||||
|
284
wideq/core.py
284
wideq/core.py
@ -1,6 +1,7 @@
|
|||||||
"""A low-level, general abstraction for the LG SmartThinQ API.
|
"""A low-level, general abstraction for the LG SmartThinQ API.
|
||||||
"""
|
"""
|
||||||
import base64
|
import base64
|
||||||
|
from enum import Enum
|
||||||
import uuid
|
import uuid
|
||||||
from urllib.parse import urljoin, urlencode, urlparse, parse_qs
|
from urllib.parse import urljoin, urlencode, urlparse, parse_qs
|
||||||
import hashlib
|
import hashlib
|
||||||
@ -12,18 +13,39 @@ from typing import Any, Dict, List, Tuple
|
|||||||
from requests.adapters import HTTPAdapter
|
from requests.adapters import HTTPAdapter
|
||||||
from requests.packages.urllib3.util.retry import Retry
|
from requests.packages.urllib3.util.retry import Retry
|
||||||
|
|
||||||
GATEWAY_URL = "https://kic.lgthinq.com:46030/api/common/gatewayUriList"
|
GATEWAY_URL = (
|
||||||
APP_KEY = "wideq"
|
"https://route.lgthinq.com:46030/v1/service/application/gateway-uri"
|
||||||
|
)
|
||||||
SECURITY_KEY = "nuts_securitykey"
|
SECURITY_KEY = "nuts_securitykey"
|
||||||
DATA_ROOT = "lgedmRoot"
|
APP_KEY = "wideq"
|
||||||
|
DATA_ROOT = "result"
|
||||||
|
POST_DATA_ROOT = "lgedmRoot"
|
||||||
|
RETURN_CODE_ROOT = "resultCode"
|
||||||
|
RETURN_MESSAGE_ROOT = "returnMsg"
|
||||||
SVC_CODE = "SVC202"
|
SVC_CODE = "SVC202"
|
||||||
CLIENT_ID = "LGAO221A02"
|
|
||||||
OAUTH_SECRET_KEY = "c053c2a6ddeb7ad97cb0eed0dcb31cf8"
|
OAUTH_SECRET_KEY = "c053c2a6ddeb7ad97cb0eed0dcb31cf8"
|
||||||
OAUTH_CLIENT_KEY = "LGAO221A02"
|
OAUTH_CLIENT_KEY = "LGAO221A02"
|
||||||
DATE_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
|
DATE_FORMAT = "%a, %d %b %Y %H:%M:%S +0000"
|
||||||
DEFAULT_COUNTRY = "US"
|
DEFAULT_COUNTRY = "US"
|
||||||
DEFAULT_LANGUAGE = "en-US"
|
DEFAULT_LANGUAGE = "en-US"
|
||||||
|
|
||||||
|
# v2
|
||||||
|
API_KEY = "VGhpblEyLjAgU0VSVklDRQ=="
|
||||||
|
|
||||||
|
# the client id is a SHA512 hash of the phone MFR,MODEL,SERIAL,
|
||||||
|
# and the build id of the thinq app it can also just be a random
|
||||||
|
# string, we use the same client id used for oauth
|
||||||
|
CLIENT_ID = OAUTH_CLIENT_KEY
|
||||||
|
MESSAGE_ID = "wideq"
|
||||||
|
SVC_PHASE = "OP"
|
||||||
|
APP_LEVEL = "PRD"
|
||||||
|
APP_OS = "ANDROID"
|
||||||
|
APP_TYPE = "NUTS"
|
||||||
|
APP_VER = "3.5.1200"
|
||||||
|
|
||||||
|
|
||||||
|
OAUTH_REDIRECT_URI = "https://kr.m.lgaccount.com/login/iabClose"
|
||||||
|
|
||||||
RETRY_COUNT = 5 # Anecdotally this seems sufficient.
|
RETRY_COUNT = 5 # Anecdotally this seems sufficient.
|
||||||
RETRY_FACTOR = 0.5
|
RETRY_FACTOR = 0.5
|
||||||
RETRY_STATUSES = (502, 503, 504)
|
RETRY_STATUSES = (502, 503, 504)
|
||||||
@ -129,7 +151,7 @@ def get_list(obj, key: str) -> List[Dict[str, Any]]:
|
|||||||
class APIError(Exception):
|
class APIError(Exception):
|
||||||
"""An error reported by the API."""
|
"""An error reported by the API."""
|
||||||
|
|
||||||
def __init__(self, code, message):
|
def __init__(self, code, message=None):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
@ -159,6 +181,10 @@ class InvalidRequestError(APIError):
|
|||||||
"""The server rejected a request as invalid."""
|
"""The server rejected a request as invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceNotFoundError:
|
||||||
|
"""The device couldn't be found."""
|
||||||
|
|
||||||
|
|
||||||
class MonitorError(APIError):
|
class MonitorError(APIError):
|
||||||
"""Monitoring a device failed, possibly because the monitoring
|
"""Monitoring a device failed, possibly because the monitoring
|
||||||
session failed and needs to be restarted.
|
session failed and needs to be restarted.
|
||||||
@ -185,7 +211,21 @@ API_ERRORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def lgedm_post(url, data=None, access_token=None, session_id=None):
|
class RequestMethod(Enum):
|
||||||
|
GET = "get"
|
||||||
|
POST = "post"
|
||||||
|
|
||||||
|
|
||||||
|
def thinq_request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data=None,
|
||||||
|
access_token=None,
|
||||||
|
session_id=None,
|
||||||
|
user_number=None,
|
||||||
|
country=DEFAULT_COUNTRY,
|
||||||
|
language=DEFAULT_LANGUAGE,
|
||||||
|
):
|
||||||
"""Make an HTTP request in the format used by the API servers.
|
"""Make an HTTP request in the format used by the API servers.
|
||||||
|
|
||||||
In this format, the request POST data sent as JSON under a special
|
In this format, the request POST data sent as JSON under a special
|
||||||
@ -197,30 +237,45 @@ def lgedm_post(url, data=None, access_token=None, session_id=None):
|
|||||||
the gateway server data or to start a session.
|
the gateway server data or to start a session.
|
||||||
"""
|
"""
|
||||||
headers = {
|
headers = {
|
||||||
"x-thinq-application-key": APP_KEY,
|
|
||||||
"x-thinq-security-key": SECURITY_KEY,
|
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
|
"x-api-key": API_KEY,
|
||||||
|
"x-client-id": CLIENT_ID,
|
||||||
|
"x-country-code": country,
|
||||||
|
"x-language-code": language,
|
||||||
|
"x-message-id": MESSAGE_ID,
|
||||||
|
"x-service-code": SVC_CODE,
|
||||||
|
"x-service-phase": SVC_PHASE,
|
||||||
|
"x-thinq-app-level": APP_LEVEL,
|
||||||
|
"x-thinq-app-os": APP_OS,
|
||||||
|
"x-thinq-app-type": APP_TYPE,
|
||||||
|
"x-thinq-app-ver": APP_VER,
|
||||||
}
|
}
|
||||||
|
|
||||||
if access_token:
|
if access_token:
|
||||||
headers["x-thinq-token"] = access_token
|
headers["x-emp-token"] = access_token
|
||||||
if session_id:
|
if user_number:
|
||||||
headers["x-thinq-jsessionId"] = session_id
|
headers["x-user-no"] = user_number
|
||||||
|
|
||||||
with retry_session() as session:
|
with retry_session() as session:
|
||||||
res = session.post(url, json={DATA_ROOT: data}, headers=headers)
|
if method == RequestMethod.POST:
|
||||||
out = res.json()[DATA_ROOT]
|
res = session.post(url, json=data, headers=headers)
|
||||||
|
elif method == RequestMethod.GET:
|
||||||
|
res = session.get(url, headers=headers)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported request method")
|
||||||
|
|
||||||
|
out = res.json()
|
||||||
|
|
||||||
# Check for API errors.
|
# Check for API errors.
|
||||||
if "returnCd" in out:
|
if RETURN_CODE_ROOT in out:
|
||||||
code = out["returnCd"]
|
code = out[RETURN_CODE_ROOT]
|
||||||
if code != "0000":
|
if code != "0000":
|
||||||
message = out["returnMsg"]
|
|
||||||
if code in API_ERRORS:
|
if code in API_ERRORS:
|
||||||
raise API_ERRORS[code](code, message)
|
raise API_ERRORS[code](code)
|
||||||
else:
|
else:
|
||||||
raise APIError(code, message)
|
raise APIError(code)
|
||||||
|
|
||||||
return out
|
return out[DATA_ROOT]
|
||||||
|
|
||||||
|
|
||||||
def oauth_url(auth_base, country, language):
|
def oauth_url(auth_base, country, language):
|
||||||
@ -228,18 +283,20 @@ def oauth_url(auth_base, country, language):
|
|||||||
authenticated session.
|
authenticated session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = urljoin(auth_base, "login/sign_in")
|
url = urljoin(auth_base, "spx/login/signIn")
|
||||||
query = urlencode(
|
query = urlencode(
|
||||||
{
|
{
|
||||||
"country": country,
|
"country": country,
|
||||||
"language": language,
|
"language": language,
|
||||||
"svcCode": SVC_CODE,
|
"svc_list": SVC_CODE,
|
||||||
"authSvr": "oauth2",
|
|
||||||
"client_id": CLIENT_ID,
|
"client_id": CLIENT_ID,
|
||||||
"division": "ha",
|
"division": "ha",
|
||||||
"grant_type": "password",
|
"redirect_uri": OAUTH_REDIRECT_URI,
|
||||||
|
"state": uuid.uuid1().hex,
|
||||||
|
"show_thirdparty_login": "AMZ,FBK",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return "{}?{}".format(url, query)
|
return "{}?{}".format(url, query)
|
||||||
|
|
||||||
|
|
||||||
@ -250,35 +307,37 @@ def parse_oauth_callback(url):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
params = parse_qs(urlparse(url).query)
|
params = parse_qs(urlparse(url).query)
|
||||||
return params["access_token"][0], params["refresh_token"][0]
|
return (
|
||||||
|
params["oauth2_backend_url"][0],
|
||||||
|
params["code"][0],
|
||||||
|
params["user_number"][0],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def login(api_root, access_token, country, language):
|
class OAuthGrant(Enum):
|
||||||
"""Use an access token to log into the API and obtain a session and
|
REFRESH_TOKEN = "refresh_token"
|
||||||
return information about the session.
|
AUTHORIZATION_CODE = "authorization_code"
|
||||||
"""
|
|
||||||
|
|
||||||
url = urljoin(api_root + "/", "member/login")
|
|
||||||
data = {
|
|
||||||
"countryCode": country,
|
|
||||||
"langCode": language,
|
|
||||||
"loginType": "EMP",
|
|
||||||
"token": access_token,
|
|
||||||
}
|
|
||||||
return lgedm_post(url, data)
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_auth(oauth_root, refresh_token):
|
def oauth_request(grant, oauth_root, token):
|
||||||
"""Get a new access_token using a refresh_token.
|
"""Make an oauth_request with a specific grant type
|
||||||
|
|
||||||
May raise a `TokenError`.
|
May raise a `TokenError`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
token_url = urljoin(oauth_root, "/oauth2/token")
|
oauth_path = "/oauth/1.0/oauth2/token"
|
||||||
data = {
|
token_url = urljoin(oauth_root, oauth_path)
|
||||||
"grant_type": "refresh_token",
|
data = {}
|
||||||
"refresh_token": refresh_token,
|
|
||||||
}
|
if grant == OAuthGrant.REFRESH_TOKEN:
|
||||||
|
data["grant_type"] = "refresh_token"
|
||||||
|
data["refresh_token"] = token
|
||||||
|
elif grant == OAuthGrant.AUTHORIZATION_CODE:
|
||||||
|
data["code"] = token
|
||||||
|
data["grant_type"] = "authorization_code"
|
||||||
|
data["redirect_uri"] = OAUTH_REDIRECT_URI
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported grant")
|
||||||
|
|
||||||
# The timestamp for labeling OAuth requests can be obtained
|
# The timestamp for labeling OAuth requests can be obtained
|
||||||
# through a request to the date/time endpoint:
|
# through a request to the date/time endpoint:
|
||||||
@ -289,17 +348,15 @@ def refresh_auth(oauth_root, refresh_token):
|
|||||||
# The signature for the requests is on a string consisting of two
|
# The signature for the requests is on a string consisting of two
|
||||||
# parts: (1) a fake request URL containing the refresh token, and (2)
|
# parts: (1) a fake request URL containing the refresh token, and (2)
|
||||||
# the timestamp.
|
# the timestamp.
|
||||||
req_url = (
|
req_url = "{}?{}".format(oauth_path, urlencode(data))
|
||||||
"/oauth2/token?grant_type=refresh_token&refresh_token=" + refresh_token
|
|
||||||
)
|
|
||||||
sig = oauth2_signature(
|
sig = oauth2_signature(
|
||||||
"{}\n{}".format(req_url, timestamp), OAUTH_SECRET_KEY
|
"{}\n{}".format(req_url, timestamp), OAUTH_SECRET_KEY
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"lgemp-x-app-key": OAUTH_CLIENT_KEY,
|
"x-lge-appkey": CLIENT_ID,
|
||||||
"lgemp-x-signature": sig,
|
"x-lge-oauth-signature": sig,
|
||||||
"lgemp-x-date": timestamp,
|
"x-lge-oauth-date": timestamp,
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,16 +364,16 @@ def refresh_auth(oauth_root, refresh_token):
|
|||||||
res = session.post(token_url, data=data, headers=headers)
|
res = session.post(token_url, data=data, headers=headers)
|
||||||
res_data = res.json()
|
res_data = res.json()
|
||||||
|
|
||||||
if res_data["status"] != 1:
|
if res.status_code != 200:
|
||||||
raise TokenError()
|
raise TokenError()
|
||||||
return res_data["access_token"]
|
|
||||||
|
return res_data
|
||||||
|
|
||||||
|
|
||||||
class Gateway(object):
|
class Gateway(object):
|
||||||
def __init__(self, auth_base, api_root, oauth_root, country, language):
|
def __init__(self, auth_base, api_root, country, language):
|
||||||
self.auth_base = auth_base
|
self.auth_base = auth_base
|
||||||
self.api_root = api_root
|
self.api_root = api_root
|
||||||
self.oauth_root = oauth_root
|
|
||||||
self.country = country
|
self.country = country
|
||||||
self.language = language
|
self.language = language
|
||||||
|
|
||||||
@ -327,12 +384,12 @@ class Gateway(object):
|
|||||||
`country` and `language` are codes, like "US" and "en-US,"
|
`country` and `language` are codes, like "US" and "en-US,"
|
||||||
respectively.
|
respectively.
|
||||||
"""
|
"""
|
||||||
gw = lgedm_post(
|
gw = thinq_request(
|
||||||
GATEWAY_URL, {"countryCode": country, "langCode": language}
|
RequestMethod.GET,
|
||||||
)
|
GATEWAY_URL,
|
||||||
return cls(
|
{"countryCode": country, "langCode": language},
|
||||||
gw["empUri"], gw["thinqUri"], gw["oauthUri"], country, language
|
|
||||||
)
|
)
|
||||||
|
return cls(gw["empUri"], gw["thinq2Uri"], country, language)
|
||||||
|
|
||||||
def oauth_url(self):
|
def oauth_url(self):
|
||||||
return oauth_url(self.auth_base, self.country, self.language)
|
return oauth_url(self.auth_base, self.country, self.language)
|
||||||
@ -341,7 +398,6 @@ class Gateway(object):
|
|||||||
return {
|
return {
|
||||||
"auth_base": self.auth_base,
|
"auth_base": self.auth_base,
|
||||||
"api_root": self.api_root,
|
"api_root": self.api_root,
|
||||||
"oauth_root": self.oauth_root,
|
|
||||||
"country": self.country,
|
"country": self.country,
|
||||||
"language": self.language,
|
"language": self.language,
|
||||||
}
|
}
|
||||||
@ -351,56 +407,68 @@ class Gateway(object):
|
|||||||
return cls(
|
return cls(
|
||||||
data["auth_base"],
|
data["auth_base"],
|
||||||
data["api_root"],
|
data["api_root"],
|
||||||
data["oauth_root"],
|
|
||||||
data.get("country", DEFAULT_COUNTRY),
|
data.get("country", DEFAULT_COUNTRY),
|
||||||
data.get("language", DEFAULT_LANGUAGE),
|
data.get("language", DEFAULT_LANGUAGE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Auth(object):
|
class Auth(object):
|
||||||
def __init__(self, gateway, access_token, refresh_token):
|
def __init__(
|
||||||
|
self, gateway, access_token, refresh_token, user_number, oauth_root
|
||||||
|
):
|
||||||
self.gateway = gateway
|
self.gateway = gateway
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
self.refresh_token = refresh_token
|
self.refresh_token = refresh_token
|
||||||
|
self.user_number = user_number
|
||||||
|
self.oauth_root = oauth_root
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_url(cls, gateway, url):
|
def from_url(cls, gateway, url):
|
||||||
"""Create an authentication using an OAuth callback URL."""
|
"""Create an authentication using an OAuth callback URL."""
|
||||||
|
|
||||||
access_token, refresh_token = parse_oauth_callback(url)
|
oauth_root, auth_code, user_number = parse_oauth_callback(url)
|
||||||
return cls(gateway, access_token, refresh_token)
|
out = oauth_request(
|
||||||
|
OAuthGrant.AUTHORIZATION_CODE, oauth_root, auth_code
|
||||||
|
)
|
||||||
|
return cls(
|
||||||
|
gateway,
|
||||||
|
out["access_token"],
|
||||||
|
out["refresh_token"],
|
||||||
|
user_number,
|
||||||
|
oauth_root,
|
||||||
|
)
|
||||||
|
|
||||||
def start_session(self) -> Tuple["Session", List[Dict[str, Any]]]:
|
def start_session(self) -> Tuple["Session", List[Dict[str, Any]]]:
|
||||||
"""Start an API session for the logged-in user. Return the
|
"""Start an API session for the logged-in user. Return the
|
||||||
Session object and a list of the user's devices.
|
Session object and a list of the user's devices.
|
||||||
"""
|
"""
|
||||||
|
return Session(self), []
|
||||||
session_info = login(
|
|
||||||
self.gateway.api_root,
|
|
||||||
self.access_token,
|
|
||||||
self.gateway.country,
|
|
||||||
self.gateway.language,
|
|
||||||
)
|
|
||||||
session_id = session_info["jsessionId"]
|
|
||||||
return Session(self, session_id), get_list(session_info, "item")
|
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh the authentication, returning a new Auth object."""
|
"""Refresh the authentication, returning a new Auth object."""
|
||||||
|
|
||||||
new_access_token = refresh_auth(
|
new_access_token = oauth_request(
|
||||||
self.gateway.oauth_root, self.refresh_token
|
OAuthGrant.REFRESH_TOKEN, self.oauth_root, self.refresh_token
|
||||||
|
)["access_token"]
|
||||||
|
return Auth(
|
||||||
|
self.gateway,
|
||||||
|
new_access_token,
|
||||||
|
self.refresh_token,
|
||||||
|
self.user_number,
|
||||||
|
self.oauth_root,
|
||||||
)
|
)
|
||||||
return Auth(self.gateway, new_access_token, self.refresh_token)
|
|
||||||
|
|
||||||
def serialize(self) -> Dict[str, str]:
|
def serialize(self) -> Dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"access_token": self.access_token,
|
"access_token": self.access_token,
|
||||||
"refresh_token": self.refresh_token,
|
"refresh_token": self.refresh_token,
|
||||||
|
"user_number": self.user_number,
|
||||||
|
"oauth_root": self.oauth_root,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
def __init__(self, auth, session_id) -> None:
|
def __init__(self, auth, session_id=None) -> None:
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
|
|
||||||
@ -412,7 +480,30 @@ class Session(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
url = urljoin(self.auth.gateway.api_root + "/", path)
|
url = urljoin(self.auth.gateway.api_root + "/", path)
|
||||||
return lgedm_post(url, data, self.auth.access_token, self.session_id)
|
return thinq_request(
|
||||||
|
RequestMethod.POST,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
self.auth.access_token,
|
||||||
|
user_number=self.auth.user_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, path):
|
||||||
|
"""Make a GET request to the API server.
|
||||||
|
|
||||||
|
This is like `lgedm_get`, but it pulls the context for the
|
||||||
|
request from an active Session.
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = urljoin(self.auth.gateway.api_root + "/", path)
|
||||||
|
return thinq_request(
|
||||||
|
RequestMethod.GET,
|
||||||
|
url,
|
||||||
|
access_token=self.auth.access_token,
|
||||||
|
user_number=self.auth.user_number,
|
||||||
|
country=self.auth.gateway.country,
|
||||||
|
language=self.auth.gateway.language,
|
||||||
|
)
|
||||||
|
|
||||||
def get_devices(self) -> List[Dict[str, Any]]:
|
def get_devices(self) -> List[Dict[str, Any]]:
|
||||||
"""Get a list of devices associated with the user's account.
|
"""Get a list of devices associated with the user's account.
|
||||||
@ -420,7 +511,7 @@ class Session(object):
|
|||||||
Return a list of dicts with information about the devices.
|
Return a list of dicts with information about the devices.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return get_list(self.post("device/deviceList"), "item")
|
return get_list(self.get("service/application/dashboard"), "item")
|
||||||
|
|
||||||
def monitor_start(self, device_id):
|
def monitor_start(self, device_id):
|
||||||
"""Begin monitoring a device's status.
|
"""Begin monitoring a device's status.
|
||||||
@ -488,40 +579,13 @@ class Session(object):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_device_controls(self, device_id, values):
|
def device_control(self, device_id, data):
|
||||||
"""Control a device's settings.
|
"""Control a device's settings.
|
||||||
|
|
||||||
`values` is a key/value map containing the settings to update.
|
`values` is a key/value map containing the settings to update.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.post(
|
controlPath = "service/devices/{}/control-sync".format(device_id)
|
||||||
"rti/rtiControl",
|
|
||||||
{
|
|
||||||
"cmd": "Control",
|
|
||||||
"cmdOpt": "Set",
|
|
||||||
"value": values,
|
|
||||||
"deviceId": device_id,
|
|
||||||
"workId": gen_uuid(),
|
|
||||||
"data": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_device_config(self, device_id, key, category="Config"):
|
res = self.post(controlPath, data)
|
||||||
"""Get a device configuration option.
|
return res
|
||||||
|
|
||||||
The `category` string should probably either be "Config" or
|
|
||||||
"Control"; the right choice appears to depend on the key.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.post(
|
|
||||||
"rti/rtiControl",
|
|
||||||
{
|
|
||||||
"cmd": category,
|
|
||||||
"cmdOpt": "Get",
|
|
||||||
"value": key,
|
|
||||||
"deviceId": device_id,
|
|
||||||
"workId": gen_uuid(),
|
|
||||||
"data": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return res["returnData"]
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user