mirror of
https://github.com/no2chem/wideq.git
synced 2025-05-15 23:00:18 -07:00
introduce black formatter
This commit is contained in:
parent
6e4f9d6484
commit
fb3c755c2e
5
.flake8
Normal file
5
.flake8
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[flake8]
|
||||||
|
ignore = E203, E266, E501, W503, F403, F401
|
||||||
|
max-line-length = 79
|
||||||
|
max-complexity = 18
|
||||||
|
select = B,C,E,F,W,T4,B9
|
10
.pre-commit-config.yaml
Normal file
10
.pre-commit-config.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/ambv/black
|
||||||
|
rev: stable
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3.9
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v1.2.3
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
@ -30,6 +30,11 @@ You can also specify one of several other commands:
|
|||||||
* `turn <ID> <ONOFF>`: Turn an AC device on or off. Use "on" or "off" as the second argument.
|
* `turn <ID> <ONOFF>`: Turn an AC device on or off. Use "on" or "off" as the second argument.
|
||||||
* `ac-config <ID>`: Print out some configuration information about an AC device.
|
* `ac-config <ID>`: Print out some configuration information about an AC device.
|
||||||
|
|
||||||
|
Development
|
||||||
|
-------
|
||||||
|
To ensure consistent formatting across pull requests, install the precommit hooks to auto format your code using `pre-commit install`.
|
||||||
|
|
||||||
|
The code will be auto-formatted by `black` to ensure consistent style.
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
-------
|
-------
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["flit"]
|
requires = ["flit", "pre-commit"]
|
||||||
build-backend = "flit.buildapi"
|
build-backend = "flit.buildapi"
|
||||||
|
|
||||||
[tool.flit.metadata]
|
[tool.flit.metadata]
|
||||||
@ -16,3 +16,19 @@ requires-python = ">=3.6"
|
|||||||
test = [
|
test = [
|
||||||
"responses"
|
"responses"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
@ -1,148 +1,151 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from wideq.client import (
|
from wideq.client import (
|
||||||
BitValue, EnumValue, ModelInfo, RangeValue, ReferenceValue, StringValue)
|
BitValue,
|
||||||
|
EnumValue,
|
||||||
|
ModelInfo,
|
||||||
|
RangeValue,
|
||||||
|
ReferenceValue,
|
||||||
|
StringValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DATA = {
|
DATA = {
|
||||||
'Value': {
|
"Value": {
|
||||||
'AntiBacterial': {
|
"AntiBacterial": {
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'label': '@WM_DRY27_BUTTON_ANTI_BACTERIAL_W',
|
"label": "@WM_DRY27_BUTTON_ANTI_BACTERIAL_W",
|
||||||
'option': {
|
"option": {"0": "@CP_OFF_EN_W", "1": "@CP_ON_EN_W"},
|
||||||
'0': '@CP_OFF_EN_W',
|
"type": "Enum",
|
||||||
'1': '@CP_ON_EN_W'
|
|
||||||
},
|
|
||||||
'type': 'Enum'
|
|
||||||
},
|
},
|
||||||
'Course': {
|
"Course": {
|
||||||
'option': ['Course'],
|
"option": ["Course"],
|
||||||
'type': 'Reference',
|
"type": "Reference",
|
||||||
},
|
},
|
||||||
'Initial_Time_H': {
|
"Initial_Time_H": {
|
||||||
'default': 0,
|
"default": 0,
|
||||||
'option': {'max': 24, 'min': 0},
|
"option": {"max": 24, "min": 0},
|
||||||
'type': 'Range'
|
"type": "Range",
|
||||||
},
|
},
|
||||||
'Option1': {
|
"Option1": {
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'option': [
|
"option": [
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 0,
|
"startbit": 0,
|
||||||
'value': 'ChildLock'
|
"value": "ChildLock",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 1,
|
"startbit": 1,
|
||||||
'value': 'ReduceStatic'
|
"value": "ReduceStatic",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 2,
|
"startbit": 2,
|
||||||
'value': 'EasyIron'
|
"value": "EasyIron",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 3,
|
"startbit": 3,
|
||||||
'value': 'DampDrySingal'
|
"value": "DampDrySingal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 4,
|
"startbit": 4,
|
||||||
'value': 'WrinkleCare'
|
"value": "WrinkleCare",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'default': '0',
|
"default": "0",
|
||||||
'length': 1,
|
"length": 1,
|
||||||
'startbit': 7,
|
"startbit": 7,
|
||||||
'value': 'AntiBacterial'
|
"value": "AntiBacterial",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
'type': 'Bit'
|
"type": "Bit",
|
||||||
},
|
},
|
||||||
'TimeBsOn': {
|
"TimeBsOn": {
|
||||||
'_comment':
|
"_comment": "오전 12시 30분은 0030, 오후12시30분은 1230 ,오후 4시30분은 1630 off는 0 ",
|
||||||
'오전 12시 30분은 0030, 오후12시30분은 1230 ,오후 4시30분은 1630 off는 0 ',
|
"type": "String",
|
||||||
'type': 'String'
|
|
||||||
},
|
|
||||||
'Unexpected': {'type': 'Unexpected'},
|
|
||||||
'Unexpected2': {
|
|
||||||
'type': 'Unexpected',
|
|
||||||
'option': 'some option'
|
|
||||||
},
|
},
|
||||||
|
"Unexpected": {"type": "Unexpected"},
|
||||||
|
"Unexpected2": {"type": "Unexpected", "option": "some option"},
|
||||||
},
|
},
|
||||||
'Course': {
|
"Course": {
|
||||||
"3": {
|
"3": {
|
||||||
"_comment": "Normal",
|
"_comment": "Normal",
|
||||||
"courseType": "Course",
|
"courseType": "Course",
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "@WM_DRY27_COURSE_NORMAL_W",
|
"name": "@WM_DRY27_COURSE_NORMAL_W",
|
||||||
"script": "",
|
"script": "",
|
||||||
"controlEnable": True,
|
"controlEnable": True,
|
||||||
"freshcareEnable": True,
|
"freshcareEnable": True,
|
||||||
"imgIndex": 61,
|
"imgIndex": 61,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ModelInfoTest(unittest.TestCase):
|
class ModelInfoTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.model_info = ModelInfo(DATA)
|
self.model_info = ModelInfo(DATA)
|
||||||
|
|
||||||
def test_value_enum(self):
|
def test_value_enum(self):
|
||||||
actual = self.model_info.value('AntiBacterial')
|
actual = self.model_info.value("AntiBacterial")
|
||||||
expected = EnumValue({'0': '@CP_OFF_EN_W', '1': '@CP_ON_EN_W'})
|
expected = EnumValue({"0": "@CP_OFF_EN_W", "1": "@CP_ON_EN_W"})
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_value_range(self):
|
def test_value_range(self):
|
||||||
actual = self.model_info.value('Initial_Time_H')
|
actual = self.model_info.value("Initial_Time_H")
|
||||||
expected = RangeValue(min=0, max=24, step=1)
|
expected = RangeValue(min=0, max=24, step=1)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_value_bit(self):
|
def test_value_bit(self):
|
||||||
actual = self.model_info.value('Option1')
|
actual = self.model_info.value("Option1")
|
||||||
expected = BitValue({
|
expected = BitValue(
|
||||||
0: 'ChildLock',
|
{
|
||||||
1: 'ReduceStatic',
|
0: "ChildLock",
|
||||||
2: 'EasyIron',
|
1: "ReduceStatic",
|
||||||
3: 'DampDrySingal',
|
2: "EasyIron",
|
||||||
4: 'WrinkleCare',
|
3: "DampDrySingal",
|
||||||
7: 'AntiBacterial',
|
4: "WrinkleCare",
|
||||||
})
|
7: "AntiBacterial",
|
||||||
|
}
|
||||||
|
)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_value_reference(self):
|
def test_value_reference(self):
|
||||||
actual = self.model_info.value('Course')
|
actual = self.model_info.value("Course")
|
||||||
expected = ReferenceValue(DATA['Course'])
|
expected = ReferenceValue(DATA["Course"])
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_string(self):
|
def test_string(self):
|
||||||
actual = self.model_info.value('TimeBsOn')
|
actual = self.model_info.value("TimeBsOn")
|
||||||
expected = StringValue(
|
expected = StringValue(
|
||||||
"오전 12시 30분은 0030, 오후12시30분은 1230 ,오후 4시30분은 1630 off는 0 ")
|
"오전 12시 30분은 0030, 오후12시30분은 1230 ,오후 4시30분은 1630 off는 0 "
|
||||||
|
)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_value_unsupported(self):
|
def test_value_unsupported(self):
|
||||||
data = "{'type': 'Unexpected'}"
|
data = "{'type': 'Unexpected'}"
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
ValueError,
|
ValueError,
|
||||||
f"unsupported value name: 'Unexpected' type: 'Unexpected' "
|
f"unsupported value name: 'Unexpected' type: 'Unexpected' "
|
||||||
f"data: '{data}'"):
|
f"data: '{data}'",
|
||||||
self.model_info.value('Unexpected')
|
):
|
||||||
|
self.model_info.value("Unexpected")
|
||||||
|
|
||||||
def test_value_unsupported_but_data_available(self):
|
def test_value_unsupported_but_data_available(self):
|
||||||
data = "{'type': 'Unexpected', 'option': 'some option'}"
|
data = "{'type': 'Unexpected', 'option': 'some option'}"
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
ValueError,
|
ValueError,
|
||||||
f"unsupported value name: 'Unexpected2'"
|
f"unsupported value name: 'Unexpected2'"
|
||||||
f" type: 'Unexpected' data: '{data}"):
|
f" type: 'Unexpected' data: '{data}",
|
||||||
self.model_info.value('Unexpected2')
|
):
|
||||||
|
self.model_info.value("Unexpected2")
|
||||||
|
@ -9,47 +9,52 @@ class SimpleTest(unittest.TestCase):
|
|||||||
def test_gateway_en_US(self):
|
def test_gateway_en_US(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
'https://kic.lgthinq.com:46030/api/common/gatewayUriList',
|
"https://kic.lgthinq.com:46030/api/common/gatewayUriList",
|
||||||
json={
|
json={
|
||||||
'lgedmRoot': {
|
"lgedmRoot": {
|
||||||
"thinqUri": "https://aic.lgthinq.com:46030/api",
|
"thinqUri": "https://aic.lgthinq.com:46030/api",
|
||||||
"empUri": "https://us.m.lgaccount.com",
|
"empUri": "https://us.m.lgaccount.com",
|
||||||
"oauthUri": "https://us.lgeapi.com",
|
"oauthUri": "https://us.lgeapi.com",
|
||||||
"countryCode": "US",
|
"countryCode": "US",
|
||||||
"langCode": "en-US",
|
"langCode": "en-US",
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
gatewayInstance = wideq.core.Gateway.discover('US', 'en-US')
|
gatewayInstance = wideq.core.Gateway.discover("US", "en-US")
|
||||||
self.assertEqual(len(responses.calls), 1)
|
self.assertEqual(len(responses.calls), 1)
|
||||||
self.assertEqual(gatewayInstance.country, 'US')
|
self.assertEqual(gatewayInstance.country, "US")
|
||||||
self.assertEqual(gatewayInstance.language, 'en-US')
|
self.assertEqual(gatewayInstance.language, "en-US")
|
||||||
self.assertEqual(gatewayInstance.auth_base,
|
self.assertEqual(
|
||||||
'https://us.m.lgaccount.com')
|
gatewayInstance.auth_base, "https://us.m.lgaccount.com"
|
||||||
self.assertEqual(gatewayInstance.api_root,
|
)
|
||||||
'https://aic.lgthinq.com:46030/api')
|
self.assertEqual(
|
||||||
self.assertEqual(gatewayInstance.oauth_root, 'https://us.lgeapi.com')
|
gatewayInstance.api_root, "https://aic.lgthinq.com:46030/api"
|
||||||
|
)
|
||||||
|
self.assertEqual(gatewayInstance.oauth_root, "https://us.lgeapi.com")
|
||||||
|
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_gateway_en_NO(self):
|
def test_gateway_en_NO(self):
|
||||||
responses.add(
|
responses.add(
|
||||||
responses.POST,
|
responses.POST,
|
||||||
'https://kic.lgthinq.com:46030/api/common/gatewayUriList',
|
"https://kic.lgthinq.com:46030/api/common/gatewayUriList",
|
||||||
json={
|
json={
|
||||||
'lgedmRoot': {
|
"lgedmRoot": {
|
||||||
"countryCode": "NO", "langCode": "en-NO",
|
"countryCode": "NO",
|
||||||
"thinqUri": "https://eic.lgthinq.com:46030/api",
|
"langCode": "en-NO",
|
||||||
"empUri": "https://no.m.lgaccount.com",
|
"thinqUri": "https://eic.lgthinq.com:46030/api",
|
||||||
"oauthUri": "https://no.lgeapi.com",
|
"empUri": "https://no.m.lgaccount.com",
|
||||||
|
"oauthUri": "https://no.lgeapi.com",
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
gatewayInstance = wideq.core.Gateway.discover('NO', 'en-NO')
|
gatewayInstance = wideq.core.Gateway.discover("NO", "en-NO")
|
||||||
self.assertEqual(len(responses.calls), 1)
|
self.assertEqual(len(responses.calls), 1)
|
||||||
self.assertEqual(gatewayInstance.country, 'NO')
|
self.assertEqual(gatewayInstance.country, "NO")
|
||||||
self.assertEqual(gatewayInstance.language, 'en-NO')
|
self.assertEqual(gatewayInstance.language, "en-NO")
|
||||||
self.assertEqual(gatewayInstance.auth_base,
|
self.assertEqual(
|
||||||
'https://no.m.lgaccount.com')
|
gatewayInstance.auth_base, "https://no.m.lgaccount.com"
|
||||||
self.assertEqual(gatewayInstance.api_root,
|
)
|
||||||
'https://eic.lgthinq.com:46030/api')
|
self.assertEqual(
|
||||||
self.assertEqual(gatewayInstance.oauth_root, 'https://no.lgeapi.com')
|
gatewayInstance.api_root, "https://eic.lgthinq.com:46030/api"
|
||||||
|
)
|
||||||
|
self.assertEqual(gatewayInstance.oauth_root, "https://no.lgeapi.com")
|
||||||
|
@ -2,8 +2,11 @@ import json
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from wideq.client import Client, DeviceInfo
|
from wideq.client import Client, DeviceInfo
|
||||||
from wideq.dishwasher import DishWasherDevice, DishWasherState, \
|
from wideq.dishwasher import (
|
||||||
DishWasherStatus
|
DishWasherDevice,
|
||||||
|
DishWasherState,
|
||||||
|
DishWasherStatus,
|
||||||
|
)
|
||||||
|
|
||||||
POLL_DATA = {
|
POLL_DATA = {
|
||||||
"16~19": "0",
|
"16~19": "0",
|
||||||
@ -30,22 +33,24 @@ POLL_DATA = {
|
|||||||
|
|
||||||
|
|
||||||
class DishWasherStatusTest(unittest.TestCase):
|
class DishWasherStatusTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
with open('./tests/fixtures/client.json') as fp:
|
with open("./tests/fixtures/client.json") as fp:
|
||||||
state = json.load(fp)
|
state = json.load(fp)
|
||||||
self.client = Client.load(state)
|
self.client = Client.load(state)
|
||||||
self.device_info = DeviceInfo({
|
self.device_info = DeviceInfo(
|
||||||
'alias': 'DISHWASHER',
|
{
|
||||||
'deviceId': '33330ba80-107d-11e9-96c8-0051ede8ad3c',
|
"alias": "DISHWASHER",
|
||||||
'deviceType': 204,
|
"deviceId": "33330ba80-107d-11e9-96c8-0051ede8ad3c",
|
||||||
'modelJsonUrl': (
|
"deviceType": 204,
|
||||||
'https://aic.lgthinq.com:46030/api/webContents/modelJSON?'
|
"modelJsonUrl": (
|
||||||
'modelName=D3210&countryCode=WW&contentsId='
|
"https://aic.lgthinq.com:46030/api/webContents/modelJSON?"
|
||||||
'JS0719082250749334&authKey=thinq'),
|
"modelName=D3210&countryCode=WW&contentsId="
|
||||||
'modelNm': 'D3210',
|
"JS0719082250749334&authKey=thinq"
|
||||||
})
|
),
|
||||||
|
"modelNm": "D3210",
|
||||||
|
}
|
||||||
|
)
|
||||||
self.dishwasher = DishWasherDevice(self.client, self.device_info)
|
self.dishwasher = DishWasherDevice(self.client, self.device_info)
|
||||||
|
|
||||||
def test_properties(self):
|
def test_properties(self):
|
||||||
@ -54,6 +59,6 @@ class DishWasherStatusTest(unittest.TestCase):
|
|||||||
self.assertTrue(status.is_on)
|
self.assertTrue(status.is_on)
|
||||||
self.assertEqual(119, status.remaining_time)
|
self.assertEqual(119, status.remaining_time)
|
||||||
self.assertEqual(194, status.initial_time)
|
self.assertEqual(194, status.initial_time)
|
||||||
self.assertEqual('Heavy', status.course)
|
self.assertEqual("Heavy", status.course)
|
||||||
self.assertEqual('Casseroles', status.smart_course)
|
self.assertEqual("Casseroles", status.smart_course)
|
||||||
self.assertEqual('No Error', status.error)
|
self.assertEqual("No Error", status.error)
|
||||||
|
@ -4,47 +4,55 @@ from unittest import mock
|
|||||||
|
|
||||||
from wideq.client import Client, DeviceInfo
|
from wideq.client import Client, DeviceInfo
|
||||||
from wideq.dryer import (
|
from wideq.dryer import (
|
||||||
DryerDevice, DryLevel, DryerState, DryerStatus, TempControl, TimeDry)
|
DryerDevice,
|
||||||
|
DryLevel,
|
||||||
|
DryerState,
|
||||||
|
DryerStatus,
|
||||||
|
TempControl,
|
||||||
|
TimeDry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
POLL_DATA = {
|
POLL_DATA = {
|
||||||
'Course': '2',
|
"Course": "2",
|
||||||
'CurrentDownloadCourse': '100',
|
"CurrentDownloadCourse": "100",
|
||||||
'DryLevel': '3',
|
"DryLevel": "3",
|
||||||
'Error': '0',
|
"Error": "0",
|
||||||
'Initial_Time_H': '1',
|
"Initial_Time_H": "1",
|
||||||
'Initial_Time_M': '11',
|
"Initial_Time_M": "11",
|
||||||
'LoadItem': '0',
|
"LoadItem": "0",
|
||||||
'MoreLessTime': '0',
|
"MoreLessTime": "0",
|
||||||
'Option1': '0',
|
"Option1": "0",
|
||||||
'Option2': '168',
|
"Option2": "168",
|
||||||
'PreState': '1',
|
"PreState": "1",
|
||||||
'Remain_Time_H': '0',
|
"Remain_Time_H": "0",
|
||||||
'Remain_Time_M': '54',
|
"Remain_Time_M": "54",
|
||||||
'SmartCourse': '0',
|
"SmartCourse": "0",
|
||||||
'State': '50',
|
"State": "50",
|
||||||
'TempControl': '4',
|
"TempControl": "4",
|
||||||
'TimeDry': '0',
|
"TimeDry": "0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DryerStatusTest(unittest.TestCase):
|
class DryerStatusTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
with open('./tests/fixtures/client.json') as fp:
|
with open("./tests/fixtures/client.json") as fp:
|
||||||
state = json.load(fp)
|
state = json.load(fp)
|
||||||
self.client = Client.load(state)
|
self.client = Client.load(state)
|
||||||
self.device_info = DeviceInfo({
|
self.device_info = DeviceInfo(
|
||||||
'alias': 'DRYER',
|
{
|
||||||
'deviceId': '33330ba80-107d-11e9-96c8-0051ede85d3f',
|
"alias": "DRYER",
|
||||||
'deviceType': 202,
|
"deviceId": "33330ba80-107d-11e9-96c8-0051ede85d3f",
|
||||||
'modelJsonUrl': (
|
"deviceType": 202,
|
||||||
'https://aic.lgthinq.com:46030/api/webContents/modelJSON?'
|
"modelJsonUrl": (
|
||||||
'modelName=RV13B6ES_D_US_WIFI&countryCode=WW&contentsId='
|
"https://aic.lgthinq.com:46030/api/webContents/modelJSON?"
|
||||||
'JS11260025236447318&authKey=thinq'),
|
"modelName=RV13B6ES_D_US_WIFI&countryCode=WW&contentsId="
|
||||||
'modelNm': 'RV13B6ES_D_US_WIFI',
|
"JS11260025236447318&authKey=thinq"
|
||||||
})
|
),
|
||||||
|
"modelNm": "RV13B6ES_D_US_WIFI",
|
||||||
|
}
|
||||||
|
)
|
||||||
self.dryer = DryerDevice(self.client, self.device_info)
|
self.dryer = DryerDevice(self.client, self.device_info)
|
||||||
|
|
||||||
def test_properties(self):
|
def test_properties(self):
|
||||||
@ -57,22 +65,26 @@ class DryerStatusTest(unittest.TestCase):
|
|||||||
self.assertTrue(status.is_on)
|
self.assertTrue(status.is_on)
|
||||||
self.assertEqual(54, status.remaining_time)
|
self.assertEqual(54, status.remaining_time)
|
||||||
self.assertEqual(71, status.initial_time)
|
self.assertEqual(71, status.initial_time)
|
||||||
self.assertEqual('Towels', status.course)
|
self.assertEqual("Towels", status.course)
|
||||||
self.assertEqual('Off', status.smart_course)
|
self.assertEqual("Off", status.smart_course)
|
||||||
self.assertEqual('No Error', status.error)
|
self.assertEqual("No Error", status.error)
|
||||||
self.assertEqual(TempControl.MID_HIGH, status.temperature_control)
|
self.assertEqual(TempControl.MID_HIGH, status.temperature_control)
|
||||||
self.assertEqual(TimeDry.OFF, status.time_dry)
|
self.assertEqual(TimeDry.OFF, status.time_dry)
|
||||||
|
|
||||||
@mock.patch('wideq.client.LOGGER')
|
@mock.patch("wideq.client.LOGGER")
|
||||||
def test_properties_unknown_enum_value(self, mock_logging):
|
def test_properties_unknown_enum_value(self, mock_logging):
|
||||||
"""
|
"""
|
||||||
This should not raise an error for an invalid enum value and instead
|
This should not raise an error for an invalid enum value and instead
|
||||||
use the `UNKNOWN` enum value.
|
use the `UNKNOWN` enum value.
|
||||||
"""
|
"""
|
||||||
data = dict(POLL_DATA, State='5000')
|
data = dict(POLL_DATA, State="5000")
|
||||||
status = DryerStatus(self.dryer, data)
|
status = DryerStatus(self.dryer, data)
|
||||||
self.assertEqual(DryerState.UNKNOWN, status.state)
|
self.assertEqual(DryerState.UNKNOWN, status.state)
|
||||||
expected_call = mock.call(
|
expected_call = mock.call(
|
||||||
'Value `%s` for key `%s` not in options: %s. Values from API: %s',
|
"Value `%s` for key `%s` not in options: %s. Values from API: %s",
|
||||||
'5000', 'State', mock.ANY, mock.ANY)
|
"5000",
|
||||||
|
"State",
|
||||||
|
mock.ANY,
|
||||||
|
mock.ANY,
|
||||||
|
)
|
||||||
self.assertEqual(expected_call, mock_logging.warning.call_args)
|
self.assertEqual(expected_call, mock_logging.warning.call_args)
|
||||||
|
@ -6,48 +6,50 @@ from wideq.washer import WasherDevice, WasherState, WasherStatus
|
|||||||
|
|
||||||
|
|
||||||
POLL_DATA = {
|
POLL_DATA = {
|
||||||
'APCourse': '10',
|
"APCourse": "10",
|
||||||
'DryLevel': '0',
|
"DryLevel": "0",
|
||||||
'Error': '0',
|
"Error": "0",
|
||||||
'Initial_Time_H': '0',
|
"Initial_Time_H": "0",
|
||||||
'Initial_Time_M': '58',
|
"Initial_Time_M": "58",
|
||||||
'LoadLevel': '4',
|
"LoadLevel": "4",
|
||||||
'OPCourse': '0',
|
"OPCourse": "0",
|
||||||
'Option1': '0',
|
"Option1": "0",
|
||||||
'Option2': '0',
|
"Option2": "0",
|
||||||
'Option3': '2',
|
"Option3": "2",
|
||||||
'PreState': '23',
|
"PreState": "23",
|
||||||
'Remain_Time_H': '0',
|
"Remain_Time_H": "0",
|
||||||
'Remain_Time_M': '13',
|
"Remain_Time_M": "13",
|
||||||
'Reserve_Time_H': '0',
|
"Reserve_Time_H": "0",
|
||||||
'Reserve_Time_M': '0',
|
"Reserve_Time_M": "0",
|
||||||
'RinseOption': '1',
|
"RinseOption": "1",
|
||||||
'SmartCourse': '51',
|
"SmartCourse": "51",
|
||||||
'Soil': '0',
|
"Soil": "0",
|
||||||
'SpinSpeed': '5',
|
"SpinSpeed": "5",
|
||||||
'State': '30',
|
"State": "30",
|
||||||
'TCLCount': '15',
|
"TCLCount": "15",
|
||||||
'WaterTemp': '4',
|
"WaterTemp": "4",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class WasherStatusTest(unittest.TestCase):
|
class WasherStatusTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
with open('./tests/fixtures/client.json') as fp:
|
with open("./tests/fixtures/client.json") as fp:
|
||||||
state = json.load(fp)
|
state = json.load(fp)
|
||||||
self.client = Client.load(state)
|
self.client = Client.load(state)
|
||||||
self.device_info = DeviceInfo({
|
self.device_info = DeviceInfo(
|
||||||
'alias': 'WASHER',
|
{
|
||||||
'deviceId': '33330ba80-107d-11e9-96c8-0051ede85d3f',
|
"alias": "WASHER",
|
||||||
'deviceType': 201,
|
"deviceId": "33330ba80-107d-11e9-96c8-0051ede85d3f",
|
||||||
'modelJsonUrl': (
|
"deviceType": 201,
|
||||||
'https://aic.lgthinq.com:46030/api/webContents/modelJSON?'
|
"modelJsonUrl": (
|
||||||
'modelName=F3L2CYV5W_WIFI&countryCode=WW&contentsId='
|
"https://aic.lgthinq.com:46030/api/webContents/modelJSON?"
|
||||||
'JS1217232703654216&authKey=thinq'),
|
"modelName=F3L2CYV5W_WIFI&countryCode=WW&contentsId="
|
||||||
'modelNm': 'F3L2CYV5W_WIFI',
|
"JS1217232703654216&authKey=thinq"
|
||||||
})
|
),
|
||||||
|
"modelNm": "F3L2CYV5W_WIFI",
|
||||||
|
}
|
||||||
|
)
|
||||||
self.washer = WasherDevice(self.client, self.device_info)
|
self.washer = WasherDevice(self.client, self.device_info)
|
||||||
|
|
||||||
def test_properties(self):
|
def test_properties(self):
|
||||||
@ -57,6 +59,6 @@ class WasherStatusTest(unittest.TestCase):
|
|||||||
self.assertTrue(status.is_on)
|
self.assertTrue(status.is_on)
|
||||||
self.assertEqual(13, status.remaining_time)
|
self.assertEqual(13, status.remaining_time)
|
||||||
self.assertEqual(58, status.initial_time)
|
self.assertEqual(58, status.initial_time)
|
||||||
self.assertEqual('Towels', status.course)
|
self.assertEqual("Towels", status.course)
|
||||||
self.assertEqual('SmallLoad', status.smart_course)
|
self.assertEqual("SmallLoad", status.smart_course)
|
||||||
self.assertEqual('No Error', status.error)
|
self.assertEqual("No Error", status.error)
|
||||||
|
@ -8,4 +8,4 @@ from .dryer import * # noqa
|
|||||||
from .refrigerator import * # noqa
|
from .refrigerator import * # noqa
|
||||||
from .washer import * # noqa
|
from .washer import * # noqa
|
||||||
|
|
||||||
__version__ = '1.5.0'
|
__version__ = "1.5.0"
|
||||||
|
223
wideq/ac.py
223
wideq/ac.py
@ -33,6 +33,7 @@ class ACVSwingMode(enum.Enum):
|
|||||||
|
|
||||||
All is 100.
|
All is 100.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OFF = "@OFF"
|
OFF = "@OFF"
|
||||||
ONE = "@1"
|
ONE = "@1"
|
||||||
TWO = "@2"
|
TWO = "@2"
|
||||||
@ -54,6 +55,7 @@ class ACHSwingMode(enum.Enum):
|
|||||||
|
|
||||||
All is 100.
|
All is 100.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
OFF = "@OFF"
|
OFF = "@OFF"
|
||||||
ONE = "@1"
|
ONE = "@1"
|
||||||
TWO = "@2"
|
TWO = "@2"
|
||||||
@ -83,46 +85,70 @@ class ACMode(enum.Enum):
|
|||||||
class ACFanSpeed(enum.Enum):
|
class ACFanSpeed(enum.Enum):
|
||||||
"""The fan speed for an AC/HVAC device."""
|
"""The fan speed for an AC/HVAC device."""
|
||||||
|
|
||||||
SLOW = '@AC_MAIN_WIND_STRENGTH_SLOW_W'
|
SLOW = "@AC_MAIN_WIND_STRENGTH_SLOW_W"
|
||||||
SLOW_LOW = '@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W'
|
SLOW_LOW = "@AC_MAIN_WIND_STRENGTH_SLOW_LOW_W"
|
||||||
LOW = '@AC_MAIN_WIND_STRENGTH_LOW_W'
|
LOW = "@AC_MAIN_WIND_STRENGTH_LOW_W"
|
||||||
LOW_MID = '@AC_MAIN_WIND_STRENGTH_LOW_MID_W'
|
LOW_MID = "@AC_MAIN_WIND_STRENGTH_LOW_MID_W"
|
||||||
MID = '@AC_MAIN_WIND_STRENGTH_MID_W'
|
MID = "@AC_MAIN_WIND_STRENGTH_MID_W"
|
||||||
MID_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_HIGH_W'
|
MID_HIGH = "@AC_MAIN_WIND_STRENGTH_MID_HIGH_W"
|
||||||
HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_W'
|
HIGH = "@AC_MAIN_WIND_STRENGTH_HIGH_W"
|
||||||
POWER = '@AC_MAIN_WIND_STRENGTH_POWER_W'
|
POWER = "@AC_MAIN_WIND_STRENGTH_POWER_W"
|
||||||
AUTO = '@AC_MAIN_WIND_STRENGTH_AUTO_W'
|
AUTO = "@AC_MAIN_WIND_STRENGTH_AUTO_W"
|
||||||
NATURE = '@AC_MAIN_WIND_STRENGTH_NATURE_W'
|
NATURE = "@AC_MAIN_WIND_STRENGTH_NATURE_W"
|
||||||
R_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
|
R_LOW = "@AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W"
|
||||||
R_MID = '@AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
|
R_MID = "@AC_MAIN_WIND_STRENGTH_MID_RIGHT_W"
|
||||||
R_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
|
R_HIGH = "@AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W"
|
||||||
L_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W'
|
L_LOW = "@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W"
|
||||||
L_MID = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W'
|
L_MID = "@AC_MAIN_WIND_STRENGTH_MID_LEFT_W"
|
||||||
L_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W'
|
L_HIGH = "@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W"
|
||||||
L_LOWR_LOW = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|' \
|
L_LOWR_LOW = (
|
||||||
'AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|"
|
||||||
L_LOWR_MID = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
|
)
|
||||||
L_LOWR_HIGH = '@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|' \
|
L_LOWR_MID = (
|
||||||
'AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|"
|
||||||
L_MIDR_LOW = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_MID_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
|
)
|
||||||
L_MIDR_MID = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|' \
|
L_LOWR_HIGH = (
|
||||||
'AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_LOW_LEFT_W|"
|
||||||
L_MIDR_HIGH = '@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
|
)
|
||||||
L_HIGHR_LOW = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|' \
|
L_MIDR_LOW = (
|
||||||
'AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|"
|
||||||
L_HIGHR_MID = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_MID_RIGHT_W'
|
)
|
||||||
L_HIGHR_HIGH = '@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|' \
|
L_MIDR_MID = (
|
||||||
'AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|"
|
||||||
AUTO_2 = '@AC_MAIN_WIND_STRENGTH_AUTO_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_MID_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_AUTO_RIGHT_W'
|
)
|
||||||
POWER_2 = '@AC_MAIN_WIND_STRENGTH_POWER_LEFT_W|' \
|
L_MIDR_HIGH = (
|
||||||
'AC_MAIN_WIND_STRENGTH_POWER_RIGHT_W'
|
"@AC_MAIN_WIND_STRENGTH_MID_LEFT_W|"
|
||||||
LONGPOWER = '@AC_MAIN_WIND_STRENGTH_LONGPOWER_LEFT_W|' \
|
"AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W"
|
||||||
'AC_MAIN_WIND_STRENGTH_LONGPOWER_RIGHT_W'
|
)
|
||||||
|
L_HIGHR_LOW = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_LOW_RIGHT_W"
|
||||||
|
)
|
||||||
|
L_HIGHR_MID = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_MID_RIGHT_W"
|
||||||
|
)
|
||||||
|
L_HIGHR_HIGH = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_HIGH_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_HIGH_RIGHT_W"
|
||||||
|
)
|
||||||
|
AUTO_2 = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_AUTO_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_AUTO_RIGHT_W"
|
||||||
|
)
|
||||||
|
POWER_2 = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_POWER_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_POWER_RIGHT_W"
|
||||||
|
)
|
||||||
|
LONGPOWER = (
|
||||||
|
"@AC_MAIN_WIND_STRENGTH_LONGPOWER_LEFT_W|"
|
||||||
|
"AC_MAIN_WIND_STRENGTH_LONGPOWER_RIGHT_W"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ACOp(enum.Enum):
|
class ACOp(enum.Enum):
|
||||||
@ -150,7 +176,7 @@ class ACDevice(Device):
|
|||||||
precise control requires using the custom LUT.
|
precise control requires using the custom LUT.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mapping = self.model.value('TempFahToCel').options
|
mapping = self.model.value("TempFahToCel").options
|
||||||
return {int(f): c for f, c in mapping.items()}
|
return {int(f): c for f, c in mapping.items()}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -162,7 +188,7 @@ class ACDevice(Device):
|
|||||||
are not in the other.
|
are not in the other.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mapping = self.model.value('TempCelToFah').options
|
mapping = self.model.value("TempCelToFah").options
|
||||||
out = {}
|
out = {}
|
||||||
for c, f in mapping.items():
|
for c, f in mapping.items():
|
||||||
try:
|
try:
|
||||||
@ -174,10 +200,9 @@ class ACDevice(Device):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
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("Operation").options
|
||||||
return [ACOp(o) for i, o in mapping.items()]
|
return [ACOp(o) for i, o in mapping.items()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -208,17 +233,16 @@ class ACDevice(Device):
|
|||||||
# Or, this code will never actually be reached! We can only hope. :)
|
# Or, this code will never actually be reached! We can only hope. :)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"could not determine correct 'on' operation:"
|
f"could not determine correct 'on' operation:"
|
||||||
f" too many reported operations: '{str(operations)}'")
|
f" too many reported operations: '{str(operations)}'"
|
||||||
|
)
|
||||||
|
|
||||||
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("TempCfg", 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."""
|
||||||
"""
|
|
||||||
|
|
||||||
self.set_celsius(self.f2c[f])
|
self.set_celsius(self.f2c[f])
|
||||||
|
|
||||||
@ -235,13 +259,14 @@ class ACDevice(Device):
|
|||||||
|
|
||||||
# Ensure at least one zone is enabled: we can't turn all zones
|
# Ensure at least one zone is enabled: we can't turn all zones
|
||||||
# off simultaneously.
|
# off simultaneously.
|
||||||
on_count = sum(int(zone['State']) for zone in zones)
|
on_count = sum(int(zone["State"]) for zone in zones)
|
||||||
if on_count > 0:
|
if on_count > 0:
|
||||||
zone_cmd = '/'.join(
|
zone_cmd = "/".join(
|
||||||
'{}_{}'.format(zone['No'], zone['State'])
|
"{}_{}".format(zone["No"], zone["State"])
|
||||||
for zone in zones if zone['Cfg'] == '1'
|
for zone in zones
|
||||||
|
if zone["Cfg"] == "1"
|
||||||
)
|
)
|
||||||
self._set_control('DuctZone', zone_cmd)
|
self._set_control("DuctZone", zone_cmd)
|
||||||
|
|
||||||
def get_zones(self):
|
def get_zones(self):
|
||||||
"""Get the status of the zones, including whether a zone is
|
"""Get the status of the zones, including whether a zone is
|
||||||
@ -251,85 +276,78 @@ class ACDevice(Device):
|
|||||||
`set_zones`.
|
`set_zones`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._get_config('DuctZone')
|
return self._get_config("DuctZone")
|
||||||
|
|
||||||
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("Jet", jet_opt.value)
|
||||||
self._set_control('Jet', jet_opt_value)
|
self._set_control("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("WindStrength", speed.value)
|
||||||
self._set_control('WindStrength', speed_value)
|
self._set_control("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("WDirHStep", swing.value)
|
||||||
self._set_control('WDirHStep', swing_value)
|
self._set_control("WDirHStep", 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("WDirVStep", swing.value)
|
||||||
self._set_control('WDirVStep', swing_value)
|
self._set_control("WDirVStep", 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("OpMode", mode.value)
|
||||||
self._set_control('OpMode', mode_value)
|
self._set_control("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("Operation", op.value)
|
||||||
self._set_control('Operation', op_value)
|
self._set_control("Operation", op_value)
|
||||||
|
|
||||||
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_config("Filter")
|
||||||
|
|
||||||
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_config("MFilter")
|
||||||
|
|
||||||
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_config("EnergyDesiredValue")
|
||||||
|
|
||||||
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"""
|
||||||
|
|
||||||
value = self._get_config('OutTotalInstantPower')
|
value = self._get_config("OutTotalInstantPower")
|
||||||
return value['OutTotalInstantPower']
|
return value["OutTotalInstantPower"]
|
||||||
|
|
||||||
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"""
|
||||||
|
|
||||||
value = self._get_config('InOutInstantPower')
|
value = self._get_config("InOutInstantPower")
|
||||||
return value['InOutInstantPower']
|
return value["InOutInstantPower"]
|
||||||
|
|
||||||
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')
|
value = self._get_control("DisplayControl")
|
||||||
return value == '0' # Seems backwards, but isn't.
|
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.
|
||||||
@ -339,7 +357,7 @@ class ACDevice(Device):
|
|||||||
"""Get the speaker volume level."""
|
"""Get the speaker volume level."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value = self._get_control('SpkVolume')
|
value = self._get_control("SpkVolume")
|
||||||
return int(value)
|
return int(value)
|
||||||
except FailedRequestError:
|
except FailedRequestError:
|
||||||
return 0 # Device does not support volume control.
|
return 0 # Device does not support volume control.
|
||||||
@ -353,7 +371,7 @@ class ACDevice(Device):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Abort if monitoring has not started yet.
|
# Abort if monitoring has not started yet.
|
||||||
if not hasattr(self, 'mon'):
|
if not hasattr(self, "mon"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
res = self.mon.poll_json()
|
res = self.mon.poll_json()
|
||||||
@ -364,8 +382,7 @@ class ACDevice(Device):
|
|||||||
|
|
||||||
|
|
||||||
class ACStatus(object):
|
class ACStatus(object):
|
||||||
"""Higher-level information about an AC device's current status.
|
"""Higher-level information about an AC device's current status."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ac, data):
|
def __init__(self, ac, data):
|
||||||
self.ac = ac
|
self.ac = ac
|
||||||
@ -388,7 +405,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["TempCur"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cur_f(self):
|
def temp_cur_f(self):
|
||||||
@ -396,7 +413,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["TempCfg"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_cfg_f(self):
|
def temp_cfg_f(self):
|
||||||
@ -404,23 +421,23 @@ class ACStatus(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def mode(self):
|
def mode(self):
|
||||||
return ACMode(lookup_enum('OpMode', self.data, self.ac))
|
return ACMode(lookup_enum("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("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("WDirHStep", 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("WDirVStep", self.data, self.ac))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
op = ACOp(lookup_enum('Operation', self.data, self.ac))
|
op = ACOp(lookup_enum("Operation", self.data, self.ac))
|
||||||
return op != ACOp.OFF
|
return op != ACOp.OFF
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
188
wideq/client.py
188
wideq/client.py
@ -14,7 +14,7 @@ from . import core
|
|||||||
|
|
||||||
|
|
||||||
#: Represents an unknown enum value.
|
#: Represents an unknown enum value.
|
||||||
_UNKNOWN = 'Unknown'
|
_UNKNOWN = "Unknown"
|
||||||
LOGGER = logging.getLogger("wideq.client")
|
LOGGER = logging.getLogger("wideq.client")
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class Monitor(object):
|
|||||||
def decode_json(data: bytes) -> Dict[str, Any]:
|
def decode_json(data: bytes) -> Dict[str, Any]:
|
||||||
"""Decode a bytestring that encodes JSON status data."""
|
"""Decode a bytestring that encodes JSON status data."""
|
||||||
|
|
||||||
return json.loads(data.decode('utf8'))
|
return json.loads(data.decode("utf8"))
|
||||||
|
|
||||||
def poll_json(self) -> Optional[Dict[str, Any]]:
|
def poll_json(self) -> Optional[Dict[str, Any]]:
|
||||||
"""For devices where status is reported via JSON data, get the
|
"""For devices where status is reported via JSON data, get the
|
||||||
@ -63,7 +63,7 @@ class Monitor(object):
|
|||||||
data = self.poll()
|
data = self.poll()
|
||||||
return self.decode_json(data) if data else None
|
return self.decode_json(data) if data else None
|
||||||
|
|
||||||
def __enter__(self) -> 'Monitor':
|
def __enter__(self) -> "Monitor":
|
||||||
self.start()
|
self.start()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@ -76,12 +76,14 @@ class Client(object):
|
|||||||
and allows serialization of state.
|
and allows serialization of state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(
|
||||||
gateway: Optional[core.Gateway] = None,
|
self,
|
||||||
auth: Optional[core.Auth] = None,
|
gateway: Optional[core.Gateway] = None,
|
||||||
session: Optional[core.Session] = None,
|
auth: Optional[core.Auth] = None,
|
||||||
country: str = core.DEFAULT_COUNTRY,
|
session: Optional[core.Session] = None,
|
||||||
language: str = core.DEFAULT_LANGUAGE) -> None:
|
country: str = core.DEFAULT_COUNTRY,
|
||||||
|
language: str = core.DEFAULT_LANGUAGE,
|
||||||
|
) -> None:
|
||||||
# The three steps required to get access to call the API.
|
# The three steps required to get access to call the API.
|
||||||
self._gateway: Optional[core.Gateway] = gateway
|
self._gateway: Optional[core.Gateway] = gateway
|
||||||
self._auth: Optional[core.Auth] = auth
|
self._auth: Optional[core.Auth] = auth
|
||||||
@ -120,15 +122,14 @@ class Client(object):
|
|||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self) -> Generator['DeviceInfo', None, None]:
|
def devices(self) -> Generator["DeviceInfo", None, None]:
|
||||||
"""DeviceInfo objects describing the user's devices.
|
"""DeviceInfo objects describing the user's devices."""
|
||||||
"""
|
|
||||||
|
|
||||||
if not self._devices:
|
if not self._devices:
|
||||||
self._devices = self.session.get_devices()
|
self._devices = self.session.get_devices()
|
||||||
return (DeviceInfo(d) for d in self._devices)
|
return (DeviceInfo(d) for d in self._devices)
|
||||||
|
|
||||||
def get_device(self, device_id) -> Optional['DeviceInfo']:
|
def get_device(self, device_id) -> Optional["DeviceInfo"]:
|
||||||
"""Look up a DeviceInfo object by device ID.
|
"""Look up a DeviceInfo object by device ID.
|
||||||
|
|
||||||
Return None if the device does not exist.
|
Return None if the device does not exist.
|
||||||
@ -153,37 +154,38 @@ class Client(object):
|
|||||||
classes = util.device_classes()
|
classes = util.device_classes()
|
||||||
if device_info.type in classes:
|
if device_info.type in classes:
|
||||||
return classes[device_info.type](self, device_info)
|
return classes[device_info.type](self, device_info)
|
||||||
LOGGER.debug('No specific subclass for deviceType %s, using default',
|
LOGGER.debug(
|
||||||
device_info.type)
|
"No specific subclass for deviceType %s, using default",
|
||||||
|
device_info.type,
|
||||||
|
)
|
||||||
return Device(self, device_info)
|
return Device(self, device_info)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, state: Dict[str, Any]) -> 'Client':
|
def load(cls, state: Dict[str, Any]) -> "Client":
|
||||||
"""Load a client from serialized state.
|
"""Load a client from serialized state."""
|
||||||
"""
|
|
||||||
|
|
||||||
client = cls()
|
client = cls()
|
||||||
|
|
||||||
if 'gateway' in state:
|
if "gateway" in state:
|
||||||
client._gateway = core.Gateway.deserialize(state['gateway'])
|
client._gateway = core.Gateway.deserialize(state["gateway"])
|
||||||
|
|
||||||
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"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'session' in state:
|
if "session" in state:
|
||||||
client._session = core.Session(client.auth, state['session'])
|
client._session = core.Session(client.auth, state["session"])
|
||||||
|
|
||||||
if 'model_info' in state:
|
if "model_info" in state:
|
||||||
client._model_info = state['model_info']
|
client._model_info = state["model_info"]
|
||||||
|
|
||||||
if 'country' in state:
|
if "country" in state:
|
||||||
client._country = state['country']
|
client._country = state["country"]
|
||||||
|
|
||||||
if 'language' in state:
|
if "language" in state:
|
||||||
client._language = state['language']
|
client._language = state["language"]
|
||||||
|
|
||||||
return client
|
return client
|
||||||
|
|
||||||
@ -191,20 +193,20 @@ class Client(object):
|
|||||||
"""Serialize the client state."""
|
"""Serialize the client state."""
|
||||||
|
|
||||||
out: Dict[str, Any] = {
|
out: Dict[str, Any] = {
|
||||||
'model_info': self._model_info,
|
"model_info": self._model_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._gateway:
|
if self._gateway:
|
||||||
out['gateway'] = self._gateway.serialize()
|
out["gateway"] = self._gateway.serialize()
|
||||||
|
|
||||||
if self._auth:
|
if self._auth:
|
||||||
out['auth'] = self._auth.serialize()
|
out["auth"] = self._auth.serialize()
|
||||||
|
|
||||||
if self._session:
|
if self._session:
|
||||||
out['session'] = self._session.session_id
|
out["session"] = self._session.session_id
|
||||||
|
|
||||||
out['country'] = self._country
|
out["country"] = self._country
|
||||||
out['language'] = self._language
|
out["language"] = self._language
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@ -213,8 +215,9 @@ class Client(object):
|
|||||||
self._session, self._devices = self.auth.start_session()
|
self._session, self._devices = self.auth.start_session()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_token(cls, refresh_token,
|
def from_token(
|
||||||
country=None, language=None) -> 'Client':
|
cls, refresh_token, country=None, language=None
|
||||||
|
) -> "Client":
|
||||||
"""Construct a client using just a refresh token.
|
"""Construct a client using just a refresh token.
|
||||||
|
|
||||||
This allows simpler state storage (e.g., for human-written
|
This allows simpler state storage (e.g., for human-written
|
||||||
@ -230,7 +233,7 @@ class Client(object):
|
|||||||
client.refresh()
|
client.refresh()
|
||||||
return client
|
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
|
||||||
the model's capabilities.
|
the model's capabilities.
|
||||||
"""
|
"""
|
||||||
@ -281,44 +284,42 @@ class DeviceInfo(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def model_id(self) -> str:
|
def model_id(self) -> str:
|
||||||
return self.data['modelNm']
|
return self.data["modelNm"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self) -> str:
|
def id(self) -> str:
|
||||||
return self.data['deviceId']
|
return self.data["deviceId"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model_info_url(self) -> str:
|
def model_info_url(self) -> str:
|
||||||
return self.data['modelJsonUrl']
|
return self.data["modelJsonUrl"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return str(self.data['alias'])
|
return str(self.data["alias"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> DeviceType:
|
def type(self) -> DeviceType:
|
||||||
"""The kind of device, as a `DeviceType` value."""
|
"""The kind of device, as a `DeviceType` value."""
|
||||||
|
|
||||||
return DeviceType(self.data['deviceType'])
|
return DeviceType(self.data["deviceType"])
|
||||||
|
|
||||||
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).json()
|
||||||
|
|
||||||
|
|
||||||
BitValue = namedtuple('BitValue', ['options'])
|
BitValue = namedtuple("BitValue", ["options"])
|
||||||
EnumValue = namedtuple('EnumValue', ['options'])
|
EnumValue = namedtuple("EnumValue", ["options"])
|
||||||
RangeValue = namedtuple('RangeValue', ['min', 'max', 'step'])
|
RangeValue = namedtuple("RangeValue", ["min", "max", "step"])
|
||||||
#: This is a value that is a reference to another key in the data that is at
|
#: This is a value that is a reference to another key in the data that is at
|
||||||
#: the same level as the `Value` key.
|
#: the same level as the `Value` key.
|
||||||
ReferenceValue = namedtuple('ReferenceValue', ['reference'])
|
ReferenceValue = namedtuple("ReferenceValue", ["reference"])
|
||||||
StringValue = namedtuple('StringValue', ['comment'])
|
StringValue = namedtuple("StringValue", ["comment"])
|
||||||
|
|
||||||
|
|
||||||
class ModelInfo(object):
|
class ModelInfo(object):
|
||||||
"""A description of a device model's capabilities.
|
"""A description of a device model's capabilities."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
self.data = data
|
self.data = data
|
||||||
@ -331,47 +332,51 @@ class ModelInfo(object):
|
|||||||
`ReferenceValue`, `StringValue`).
|
`ReferenceValue`, `StringValue`).
|
||||||
: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["type"] in ("Enum", "enum"):
|
||||||
return EnumValue(d['option'])
|
return EnumValue(d["option"])
|
||||||
elif d['type'] == 'Range':
|
elif d["type"] == "Range":
|
||||||
return RangeValue(
|
return RangeValue(
|
||||||
d['option']['min'], d['option']['max'],
|
d["option"]["min"],
|
||||||
d['option'].get('step', 1)
|
d["option"]["max"],
|
||||||
|
d["option"].get("step", 1),
|
||||||
)
|
)
|
||||||
elif d['type'].lower() == 'bit':
|
elif d["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["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["type"].lower() == "string":
|
||||||
return StringValue(d.get('_comment', ''))
|
return StringValue(d.get("_comment", ""))
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"unsupported value name: '{name}'"
|
f"unsupported value name: '{name}'"
|
||||||
f" type: '{str(d['type'])}' data: '{str(d)}'")
|
f" type: '{str(d['type'])}' data: '{str(d)}'"
|
||||||
|
)
|
||||||
|
|
||||||
def default(self, name):
|
def default(self, name):
|
||||||
"""Get the default value, if it exists, for a given value.
|
"""Get the default value, if it exists, for a given value."""
|
||||||
"""
|
return self.data["Value"][name]["default"]
|
||||||
return self.data['Value'][name]['default']
|
|
||||||
|
|
||||||
def enum_value(self, key, name):
|
def enum_value(self, key, name):
|
||||||
"""Look up the encoded value for a friendly enum name.
|
"""Look up the encoded value for a friendly enum name."""
|
||||||
"""
|
|
||||||
options = self.value(key).options
|
options = self.value(key).options
|
||||||
options_inv = {v: k for k, v in options.items()} # Invert the map.
|
options_inv = {v: k for k, v in options.items()} # Invert the map.
|
||||||
return options_inv[name]
|
return options_inv[name]
|
||||||
|
|
||||||
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 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', value, key, options, self.data['Value'][key]['option'])
|
"%s",
|
||||||
|
value,
|
||||||
|
key,
|
||||||
|
options,
|
||||||
|
self.data["Value"][key]["option"],
|
||||||
|
)
|
||||||
return _UNKNOWN
|
return _UNKNOWN
|
||||||
return options[value]
|
return options[value]
|
||||||
|
|
||||||
@ -386,31 +391,30 @@ class ModelInfo(object):
|
|||||||
value = str(value)
|
value = str(value)
|
||||||
reference = self.value(key).reference
|
reference = self.value(key).reference
|
||||||
if value in reference:
|
if value in reference:
|
||||||
return reference[value]['_comment']
|
return reference[value]["_comment"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def binary_monitor_data(self):
|
def binary_monitor_data(self):
|
||||||
"""Check that type of monitoring is BINARY(BYTE).
|
"""Check that type of monitoring is BINARY(BYTE)."""
|
||||||
"""
|
return self.data["Monitoring"]["type"] == "BINARY(BYTE)"
|
||||||
return self.data['Monitoring']['type'] == 'BINARY(BYTE)'
|
|
||||||
|
|
||||||
def decode_monitor_binary(self, data):
|
def decode_monitor_binary(self, data):
|
||||||
"""Decode binary encoded status data.
|
"""Decode binary encoded status data."""
|
||||||
"""
|
|
||||||
decoded = {}
|
decoded = {}
|
||||||
for item in self.data['Monitoring']['protocol']:
|
for item in self.data["Monitoring"]["protocol"]:
|
||||||
key = item['value']
|
key = item["value"]
|
||||||
value = 0
|
value = 0
|
||||||
for v in data[item['startByte']:item['startByte'] +
|
for v in data[
|
||||||
item['length']]:
|
item["startByte"] : item["startByte"] + item["length"]
|
||||||
|
]:
|
||||||
value = (value << 8) + v
|
value = (value << 8) + v
|
||||||
decoded[key] = str(value)
|
decoded[key] = str(value)
|
||||||
return decoded
|
return decoded
|
||||||
|
|
||||||
def decode_monitor_json(self, data):
|
def decode_monitor_json(self, data):
|
||||||
"""Decode a bytestring that encodes JSON status data."""
|
"""Decode a bytestring that encodes JSON status data."""
|
||||||
return json.loads(data.decode('utf8'))
|
return json.loads(data.decode("utf8"))
|
||||||
|
|
||||||
def decode_monitor(self, data):
|
def decode_monitor(self, data):
|
||||||
"""Decode status data."""
|
"""Decode status data."""
|
||||||
@ -452,15 +456,15 @@ class Device(object):
|
|||||||
self.device.id,
|
self.device.id,
|
||||||
key,
|
key,
|
||||||
)
|
)
|
||||||
data = base64.b64decode(data).decode('utf8')
|
data = base64.b64decode(data).decode("utf8")
|
||||||
try:
|
try:
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
# Sometimes, the service returns JSON wrapped in an extra
|
# Sometimes, the service returns JSON wrapped in an extra
|
||||||
# pair of curly braces. Try removing them and re-parsing.
|
# pair of curly braces. Try removing them and re-parsing.
|
||||||
LOGGER.debug('attempting to fix JSON format')
|
LOGGER.debug("attempting to fix JSON format")
|
||||||
try:
|
try:
|
||||||
return json.loads(re.sub(r'^\{(.*?)\}$', r'\1', data))
|
return json.loads(re.sub(r"^\{(.*?)\}$", r"\1", data))
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
raise core.MalformedResponseError(data)
|
raise core.MalformedResponseError(data)
|
||||||
|
|
||||||
@ -469,11 +473,11 @@ class Device(object):
|
|||||||
data = self.client.session.get_device_config(
|
data = self.client.session.get_device_config(
|
||||||
self.device.id,
|
self.device.id,
|
||||||
key,
|
key,
|
||||||
'Control',
|
"Control",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The response comes in a funky key/value format: "(key:value)".
|
# The response comes in a funky key/value format: "(key:value)".
|
||||||
_, value = data[1:-1].split(':')
|
_, value = data[1:-1].split(":")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def monitor_start(self):
|
def monitor_start(self):
|
||||||
|
271
wideq/core.py
271
wideq/core.py
@ -12,17 +12,17 @@ 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 = "https://kic.lgthinq.com:46030/api/common/gatewayUriList"
|
||||||
APP_KEY = 'wideq'
|
APP_KEY = "wideq"
|
||||||
SECURITY_KEY = 'nuts_securitykey'
|
SECURITY_KEY = "nuts_securitykey"
|
||||||
DATA_ROOT = 'lgedmRoot'
|
DATA_ROOT = "lgedmRoot"
|
||||||
SVC_CODE = 'SVC202'
|
SVC_CODE = "SVC202"
|
||||||
CLIENT_ID = 'LGAO221A02'
|
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"
|
||||||
|
|
||||||
RETRY_COUNT = 5 # Anecdotally this seems sufficient.
|
RETRY_COUNT = 5 # Anecdotally this seems sufficient.
|
||||||
RETRY_FACTOR = 0.5
|
RETRY_FACTOR = 0.5
|
||||||
@ -38,6 +38,7 @@ def get_wideq_logger() -> logging.Logger:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import colorlog # type: ignore
|
import colorlog # type: ignore
|
||||||
|
|
||||||
colorfmt = f"%(log_color)s{fmt}%(reset)s"
|
colorfmt = f"%(log_color)s{fmt}%(reset)s"
|
||||||
handler = colorlog.StreamHandler()
|
handler = colorlog.StreamHandler()
|
||||||
handler.setFormatter(
|
handler.setFormatter(
|
||||||
@ -66,8 +67,7 @@ LOGGER = get_wideq_logger()
|
|||||||
|
|
||||||
|
|
||||||
def retry_session():
|
def retry_session():
|
||||||
"""Get a Requests session that retries HTTP and HTTPS requests.
|
"""Get a Requests session that retries HTTP and HTTPS requests."""
|
||||||
"""
|
|
||||||
# Adapted from:
|
# Adapted from:
|
||||||
# https://www.peterbe.com/plog/best-practice-with-retries-with-requests
|
# https://www.peterbe.com/plog/best-practice-with-retries-with-requests
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
@ -79,8 +79,8 @@ def retry_session():
|
|||||||
status_forcelist=RETRY_STATUSES,
|
status_forcelist=RETRY_STATUSES,
|
||||||
)
|
)
|
||||||
adapter = HTTPAdapter(max_retries=retry)
|
adapter = HTTPAdapter(max_retries=retry)
|
||||||
session.mount('http://', adapter)
|
session.mount("http://", adapter)
|
||||||
session.mount('https://', adapter)
|
session.mount("https://", adapter)
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
@ -101,8 +101,8 @@ def oauth2_signature(message: str, secret: str) -> bytes:
|
|||||||
their UTF-8 equivalents.
|
their UTF-8 equivalents.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
secret_bytes = secret.encode('utf8')
|
secret_bytes = secret.encode("utf8")
|
||||||
hashed = hmac.new(secret_bytes, message.encode('utf8'), hashlib.sha1)
|
hashed = hmac.new(secret_bytes, message.encode("utf8"), hashlib.sha1)
|
||||||
digest = hashed.digest()
|
digest = hashed.digest()
|
||||||
return base64.b64encode(digest)
|
return base64.b64encode(digest)
|
||||||
|
|
||||||
@ -197,24 +197,24 @@ 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-application-key": APP_KEY,
|
||||||
'x-thinq-security-key': SECURITY_KEY,
|
"x-thinq-security-key": SECURITY_KEY,
|
||||||
'Accept': 'application/json',
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
if access_token:
|
if access_token:
|
||||||
headers['x-thinq-token'] = access_token
|
headers["x-thinq-token"] = access_token
|
||||||
if session_id:
|
if session_id:
|
||||||
headers['x-thinq-jsessionId'] = session_id
|
headers["x-thinq-jsessionId"] = session_id
|
||||||
|
|
||||||
with retry_session() as session:
|
with retry_session() as session:
|
||||||
res = session.post(url, json={DATA_ROOT: data}, headers=headers)
|
res = session.post(url, json={DATA_ROOT: data}, headers=headers)
|
||||||
out = res.json()[DATA_ROOT]
|
out = res.json()[DATA_ROOT]
|
||||||
|
|
||||||
# Check for API errors.
|
# Check for API errors.
|
||||||
if 'returnCd' in out:
|
if "returnCd" in out:
|
||||||
code = out['returnCd']
|
code = out["returnCd"]
|
||||||
if code != '0000':
|
if code != "0000":
|
||||||
message = out['returnMsg']
|
message = out["returnMsg"]
|
||||||
if code in API_ERRORS:
|
if code in API_ERRORS:
|
||||||
raise API_ERRORS[code](code, message)
|
raise API_ERRORS[code](code, message)
|
||||||
else:
|
else:
|
||||||
@ -228,17 +228,19 @@ def oauth_url(auth_base, country, language):
|
|||||||
authenticated session.
|
authenticated session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = urljoin(auth_base, 'login/sign_in')
|
url = urljoin(auth_base, "login/sign_in")
|
||||||
query = urlencode({
|
query = urlencode(
|
||||||
'country': country,
|
{
|
||||||
'language': language,
|
"country": country,
|
||||||
'svcCode': SVC_CODE,
|
"language": language,
|
||||||
'authSvr': 'oauth2',
|
"svcCode": SVC_CODE,
|
||||||
'client_id': CLIENT_ID,
|
"authSvr": "oauth2",
|
||||||
'division': 'ha',
|
"client_id": CLIENT_ID,
|
||||||
'grant_type': 'password',
|
"division": "ha",
|
||||||
})
|
"grant_type": "password",
|
||||||
return '{}?{}'.format(url, query)
|
}
|
||||||
|
)
|
||||||
|
return "{}?{}".format(url, query)
|
||||||
|
|
||||||
|
|
||||||
def parse_oauth_callback(url):
|
def parse_oauth_callback(url):
|
||||||
@ -248,7 +250,7 @@ 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["access_token"][0], params["refresh_token"][0]
|
||||||
|
|
||||||
|
|
||||||
def login(api_root, access_token, country, language):
|
def login(api_root, access_token, country, language):
|
||||||
@ -256,12 +258,12 @@ def login(api_root, access_token, country, language):
|
|||||||
return information about the session.
|
return information about the session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = urljoin(api_root + '/', 'member/login')
|
url = urljoin(api_root + "/", "member/login")
|
||||||
data = {
|
data = {
|
||||||
'countryCode': country,
|
"countryCode": country,
|
||||||
'langCode': language,
|
"langCode": language,
|
||||||
'loginType': 'EMP',
|
"loginType": "EMP",
|
||||||
'token': access_token,
|
"token": access_token,
|
||||||
}
|
}
|
||||||
return lgedm_post(url, data)
|
return lgedm_post(url, data)
|
||||||
|
|
||||||
@ -272,10 +274,10 @@ def refresh_auth(oauth_root, refresh_token):
|
|||||||
May raise a `TokenError`.
|
May raise a `TokenError`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
token_url = urljoin(oauth_root, '/oauth2/token')
|
token_url = urljoin(oauth_root, "/oauth2/token")
|
||||||
data = {
|
data = {
|
||||||
'grant_type': 'refresh_token',
|
"grant_type": "refresh_token",
|
||||||
'refresh_token': refresh_token,
|
"refresh_token": refresh_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# The timestamp for labeling OAuth requests can be obtained
|
# The timestamp for labeling OAuth requests can be obtained
|
||||||
@ -287,25 +289,27 @@ 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 = ('/oauth2/token?grant_type=refresh_token&refresh_token=' +
|
req_url = (
|
||||||
refresh_token)
|
"/oauth2/token?grant_type=refresh_token&refresh_token=" + refresh_token
|
||||||
sig = oauth2_signature('{}\n{}'.format(req_url, timestamp),
|
)
|
||||||
OAUTH_SECRET_KEY)
|
sig = oauth2_signature(
|
||||||
|
"{}\n{}".format(req_url, timestamp), OAUTH_SECRET_KEY
|
||||||
|
)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'lgemp-x-app-key': OAUTH_CLIENT_KEY,
|
"lgemp-x-app-key": OAUTH_CLIENT_KEY,
|
||||||
'lgemp-x-signature': sig,
|
"lgemp-x-signature": sig,
|
||||||
'lgemp-x-date': timestamp,
|
"lgemp-x-date": timestamp,
|
||||||
'Accept': 'application/json',
|
"Accept": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
with retry_session() as session:
|
with retry_session() as session:
|
||||||
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_data["status"] != 1:
|
||||||
raise TokenError()
|
raise TokenError()
|
||||||
return res_data['access_token']
|
return res_data["access_token"]
|
||||||
|
|
||||||
|
|
||||||
class Gateway(object):
|
class Gateway(object):
|
||||||
@ -317,34 +321,40 @@ class Gateway(object):
|
|||||||
self.language = language
|
self.language = language
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def discover(cls, country, language) -> 'Gateway':
|
def discover(cls, country, language) -> "Gateway":
|
||||||
"""Load information about the hosts to use for API interaction.
|
"""Load information about the hosts to use for API interaction.
|
||||||
|
|
||||||
`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(GATEWAY_URL,
|
gw = lgedm_post(
|
||||||
{'countryCode': country, 'langCode': language})
|
GATEWAY_URL, {"countryCode": country, "langCode": language}
|
||||||
return cls(gw['empUri'], gw['thinqUri'], gw['oauthUri'],
|
)
|
||||||
country, language)
|
return cls(
|
||||||
|
gw["empUri"], gw["thinqUri"], gw["oauthUri"], 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)
|
||||||
|
|
||||||
def serialize(self) -> Dict[str, str]:
|
def serialize(self) -> Dict[str, str]:
|
||||||
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,
|
"oauth_root": self.oauth_root,
|
||||||
'country': self.country,
|
"country": self.country,
|
||||||
'language': self.language,
|
"language": self.language,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deserialize(cls, data: Dict[str, Any]) -> 'Gateway':
|
def deserialize(cls, data: Dict[str, Any]) -> "Gateway":
|
||||||
return cls(data['auth_base'], data['api_root'], data['oauth_root'],
|
return cls(
|
||||||
data.get('country', DEFAULT_COUNTRY),
|
data["auth_base"],
|
||||||
data.get('language', DEFAULT_LANGUAGE))
|
data["api_root"],
|
||||||
|
data["oauth_root"],
|
||||||
|
data.get("country", DEFAULT_COUNTRY),
|
||||||
|
data.get("language", DEFAULT_LANGUAGE),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Auth(object):
|
class Auth(object):
|
||||||
@ -355,34 +365,37 @@ class Auth(object):
|
|||||||
|
|
||||||
@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)
|
access_token, refresh_token = parse_oauth_callback(url)
|
||||||
return cls(gateway, access_token, refresh_token)
|
return cls(gateway, access_token, refresh_token)
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
session_info = login(self.gateway.api_root, self.access_token,
|
session_info = login(
|
||||||
self.gateway.country, self.gateway.language)
|
self.gateway.api_root,
|
||||||
session_id = session_info['jsessionId']
|
self.access_token,
|
||||||
return Session(self, session_id), get_list(session_info, 'item')
|
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(self.gateway.oauth_root,
|
new_access_token = refresh_auth(
|
||||||
self.refresh_token)
|
self.gateway.oauth_root, self.refresh_token
|
||||||
|
)
|
||||||
return Auth(self.gateway, new_access_token, self.refresh_token)
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -398,7 +411,7 @@ class Session(object):
|
|||||||
request from an active Session.
|
request from an active Session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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 lgedm_post(url, data, self.auth.access_token, self.session_id)
|
||||||
|
|
||||||
def get_devices(self) -> List[Dict[str, Any]]:
|
def get_devices(self) -> List[Dict[str, Any]]:
|
||||||
@ -407,7 +420,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.post("device/deviceList"), "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.
|
||||||
@ -416,13 +429,16 @@ class Session(object):
|
|||||||
monitoring.
|
monitoring.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res = self.post('rti/rtiMon', {
|
res = self.post(
|
||||||
'cmd': 'Mon',
|
"rti/rtiMon",
|
||||||
'cmdOpt': 'Start',
|
{
|
||||||
'deviceId': device_id,
|
"cmd": "Mon",
|
||||||
'workId': gen_uuid(),
|
"cmdOpt": "Start",
|
||||||
})
|
"deviceId": device_id,
|
||||||
return res['workId']
|
"workId": gen_uuid(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res["workId"]
|
||||||
|
|
||||||
def monitor_poll(self, device_id, work_id):
|
def monitor_poll(self, device_id, work_id):
|
||||||
"""Get the result of a monitoring task.
|
"""Get the result of a monitoring task.
|
||||||
@ -435,39 +451,42 @@ class Session(object):
|
|||||||
action is probably to restart the monitoring task.
|
action is probably to restart the monitoring task.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
work_list = [{'deviceId': device_id, 'workId': work_id}]
|
work_list = [{"deviceId": device_id, "workId": work_id}]
|
||||||
res = self.post('rti/rtiResult', {'workList': work_list})['workList']
|
res = self.post("rti/rtiResult", {"workList": work_list})["workList"]
|
||||||
|
|
||||||
# When monitoring first starts, it usually takes a few
|
# When monitoring first starts, it usually takes a few
|
||||||
# iterations before data becomes available. In the initial
|
# iterations before data becomes available. In the initial
|
||||||
# "warmup" phase, `returnCode` is missing from the response.
|
# "warmup" phase, `returnCode` is missing from the response.
|
||||||
if 'returnCode' not in res:
|
if "returnCode" not in res:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Check for errors.
|
# Check for errors.
|
||||||
code = res.get('returnCode') # returnCode can be missing.
|
code = res.get("returnCode") # returnCode can be missing.
|
||||||
if code != '0000':
|
if code != "0000":
|
||||||
raise MonitorError(device_id, code)
|
raise MonitorError(device_id, code)
|
||||||
|
|
||||||
# The return data may or may not be present, depending on the
|
# The return data may or may not be present, depending on the
|
||||||
# monitoring task status.
|
# monitoring task status.
|
||||||
if 'returnData' in res:
|
if "returnData" in res:
|
||||||
# The main response payload is base64-encoded binary data in
|
# The main response payload is base64-encoded binary data in
|
||||||
# the `returnData` field. This sometimes contains JSON data
|
# the `returnData` field. This sometimes contains JSON data
|
||||||
# and sometimes other binary data.
|
# and sometimes other binary data.
|
||||||
return base64.b64decode(res['returnData'])
|
return base64.b64decode(res["returnData"])
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def monitor_stop(self, device_id, work_id):
|
def monitor_stop(self, device_id, work_id):
|
||||||
"""Stop monitoring a device."""
|
"""Stop monitoring a device."""
|
||||||
|
|
||||||
self.post('rti/rtiMon', {
|
self.post(
|
||||||
'cmd': 'Mon',
|
"rti/rtiMon",
|
||||||
'cmdOpt': 'Stop',
|
{
|
||||||
'deviceId': device_id,
|
"cmd": "Mon",
|
||||||
'workId': work_id,
|
"cmdOpt": "Stop",
|
||||||
})
|
"deviceId": device_id,
|
||||||
|
"workId": work_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def set_device_controls(self, device_id, values):
|
def set_device_controls(self, device_id, values):
|
||||||
"""Control a device's settings.
|
"""Control a device's settings.
|
||||||
@ -475,28 +494,34 @@ class Session(object):
|
|||||||
`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('rti/rtiControl', {
|
return self.post(
|
||||||
'cmd': 'Control',
|
"rti/rtiControl",
|
||||||
'cmdOpt': 'Set',
|
{
|
||||||
'value': values,
|
"cmd": "Control",
|
||||||
'deviceId': device_id,
|
"cmdOpt": "Set",
|
||||||
'workId': gen_uuid(),
|
"value": values,
|
||||||
'data': '',
|
"deviceId": device_id,
|
||||||
})
|
"workId": gen_uuid(),
|
||||||
|
"data": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def get_device_config(self, device_id, key, category='Config'):
|
def get_device_config(self, device_id, key, category="Config"):
|
||||||
"""Get a device configuration option.
|
"""Get a device configuration option.
|
||||||
|
|
||||||
The `category` string should probably either be "Config" or
|
The `category` string should probably either be "Config" or
|
||||||
"Control"; the right choice appears to depend on the key.
|
"Control"; the right choice appears to depend on the key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res = self.post('rti/rtiControl', {
|
res = self.post(
|
||||||
'cmd': category,
|
"rti/rtiControl",
|
||||||
'cmdOpt': 'Get',
|
{
|
||||||
'value': key,
|
"cmd": category,
|
||||||
'deviceId': device_id,
|
"cmdOpt": "Get",
|
||||||
'workId': gen_uuid(),
|
"value": key,
|
||||||
'data': '',
|
"deviceId": device_id,
|
||||||
})
|
"workId": gen_uuid(),
|
||||||
return res['returnData']
|
"data": "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res["returnData"]
|
||||||
|
@ -7,56 +7,58 @@ from .util import lookup_enum, lookup_reference
|
|||||||
|
|
||||||
class DishWasherState(enum.Enum):
|
class DishWasherState(enum.Enum):
|
||||||
"""The state of the dishwasher device."""
|
"""The state of the dishwasher device."""
|
||||||
INITIAL = '@DW_STATE_INITIAL_W'
|
|
||||||
RUNNING = '@DW_STATE_RUNNING_W'
|
INITIAL = "@DW_STATE_INITIAL_W"
|
||||||
|
RUNNING = "@DW_STATE_RUNNING_W"
|
||||||
PAUSED = "@DW_STATE_PAUSE_W"
|
PAUSED = "@DW_STATE_PAUSE_W"
|
||||||
OFF = '@DW_STATE_POWER_OFF_W'
|
OFF = "@DW_STATE_POWER_OFF_W"
|
||||||
COMPLETE = '@DW_STATE_COMPLETE_W'
|
COMPLETE = "@DW_STATE_COMPLETE_W"
|
||||||
POWER_FAIL = "@DW_STATE_POWER_FAIL_W"
|
POWER_FAIL = "@DW_STATE_POWER_FAIL_W"
|
||||||
|
|
||||||
|
|
||||||
DISHWASHER_STATE_READABLE = {
|
DISHWASHER_STATE_READABLE = {
|
||||||
'INITIAL': 'Standby',
|
"INITIAL": "Standby",
|
||||||
'RUNNING': 'Running',
|
"RUNNING": "Running",
|
||||||
'PAUSED': 'Paused',
|
"PAUSED": "Paused",
|
||||||
'OFF': 'Off',
|
"OFF": "Off",
|
||||||
'COMPLETE': 'Complete',
|
"COMPLETE": "Complete",
|
||||||
'POWER_FAIL': 'Power Failed'
|
"POWER_FAIL": "Power Failed",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DishWasherProcess(enum.Enum):
|
class DishWasherProcess(enum.Enum):
|
||||||
"""The process within the dishwasher state."""
|
"""The process within the dishwasher state."""
|
||||||
RESERVE = '@DW_STATE_RESERVE_W'
|
|
||||||
RUNNING = '@DW_STATE_RUNNING_W'
|
RESERVE = "@DW_STATE_RESERVE_W"
|
||||||
RINSING = '@DW_STATE_RINSING_W'
|
RUNNING = "@DW_STATE_RUNNING_W"
|
||||||
DRYING = '@DW_STATE_DRYING_W'
|
RINSING = "@DW_STATE_RINSING_W"
|
||||||
COMPLETE = '@DW_STATE_COMPLETE_W'
|
DRYING = "@DW_STATE_DRYING_W"
|
||||||
NIGHT_DRYING = '@DW_STATE_NIGHTDRY_W'
|
COMPLETE = "@DW_STATE_COMPLETE_W"
|
||||||
CANCELLED = '@DW_STATE_CANCEL_W'
|
NIGHT_DRYING = "@DW_STATE_NIGHTDRY_W"
|
||||||
|
CANCELLED = "@DW_STATE_CANCEL_W"
|
||||||
|
|
||||||
|
|
||||||
DISHWASHER_PROCESS_READABLE = {
|
DISHWASHER_PROCESS_READABLE = {
|
||||||
'RESERVE': 'Delayed Start',
|
"RESERVE": "Delayed Start",
|
||||||
'RUNNING': DISHWASHER_STATE_READABLE['RUNNING'],
|
"RUNNING": DISHWASHER_STATE_READABLE["RUNNING"],
|
||||||
'RINSING': 'Rinsing',
|
"RINSING": "Rinsing",
|
||||||
'DRYING': 'Drying',
|
"DRYING": "Drying",
|
||||||
'COMPLETE': DISHWASHER_STATE_READABLE['COMPLETE'],
|
"COMPLETE": DISHWASHER_STATE_READABLE["COMPLETE"],
|
||||||
'NIGHT_DRYING': 'Night Drying',
|
"NIGHT_DRYING": "Night Drying",
|
||||||
'CANCELLED': 'Cancelled',
|
"CANCELLED": "Cancelled",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Provide a map to correct typos in the official course names.
|
# Provide a map to correct typos in the official course names.
|
||||||
DISHWASHER_COURSE_MAP = {
|
DISHWASHER_COURSE_MAP = {
|
||||||
'Haeavy': 'Heavy',
|
"Haeavy": "Heavy",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DishWasherDevice(Device):
|
class DishWasherDevice(Device):
|
||||||
"""A higher-level interface for a dishwasher."""
|
"""A higher-level interface for a dishwasher."""
|
||||||
|
|
||||||
def poll(self) -> Optional['DishWasherStatus']:
|
def poll(self) -> Optional["DishWasherStatus"]:
|
||||||
"""Poll the device's current state.
|
"""Poll the device's current state.
|
||||||
|
|
||||||
Monitoring must be started first with `monitor_start`.
|
Monitoring must be started first with `monitor_start`.
|
||||||
@ -65,7 +67,7 @@ class DishWasherDevice(Device):
|
|||||||
is not yet available.
|
is not yet available.
|
||||||
"""
|
"""
|
||||||
# Abort if monitoring has not started yet.
|
# Abort if monitoring has not started yet.
|
||||||
if not hasattr(self, 'mon'):
|
if not hasattr(self, "mon"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.mon.poll()
|
data = self.mon.poll()
|
||||||
@ -91,7 +93,8 @@ class DishWasherStatus(object):
|
|||||||
def state(self) -> DishWasherState:
|
def state(self) -> DishWasherState:
|
||||||
"""Get the state of the dishwasher."""
|
"""Get the state of the dishwasher."""
|
||||||
return DishWasherState(
|
return DishWasherState(
|
||||||
lookup_enum('State', self.data, self.dishwasher))
|
lookup_enum("State", self.data, self.dishwasher)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def readable_state(self) -> str:
|
def readable_state(self) -> str:
|
||||||
@ -101,8 +104,8 @@ class DishWasherStatus(object):
|
|||||||
@property
|
@property
|
||||||
def process(self) -> Optional[DishWasherProcess]:
|
def process(self) -> Optional[DishWasherProcess]:
|
||||||
"""Get the process of the dishwasher."""
|
"""Get the process of the dishwasher."""
|
||||||
process = lookup_enum('Process', self.data, self.dishwasher)
|
process = lookup_enum("Process", self.data, self.dishwasher)
|
||||||
if process and process != '-':
|
if process and process != "-":
|
||||||
return DishWasherProcess(process)
|
return DishWasherProcess(process)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
@ -123,27 +126,28 @@ class DishWasherStatus(object):
|
|||||||
@property
|
@property
|
||||||
def remaining_time(self) -> int:
|
def remaining_time(self) -> int:
|
||||||
"""Get the remaining time in minutes."""
|
"""Get the remaining time in minutes."""
|
||||||
return (int(self.data['Remain_Time_H']) * 60 +
|
return int(self.data["Remain_Time_H"]) * 60 + int(
|
||||||
int(self.data['Remain_Time_M']))
|
self.data["Remain_Time_M"]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_time(self) -> int:
|
def initial_time(self) -> int:
|
||||||
"""Get the initial time in minutes."""
|
"""Get the initial time in minutes."""
|
||||||
return (
|
return int(self.data["Initial_Time_H"]) * 60 + int(
|
||||||
int(self.data['Initial_Time_H']) * 60 +
|
self.data["Initial_Time_M"]
|
||||||
int(self.data['Initial_Time_M']))
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reserve_time(self) -> int:
|
def reserve_time(self) -> int:
|
||||||
"""Get the reserve time in minutes."""
|
"""Get the reserve time in minutes."""
|
||||||
return (
|
return int(self.data["Reserve_Time_H"]) * 60 + int(
|
||||||
int(self.data['Reserve_Time_H']) * 60 +
|
self.data["Reserve_Time_M"]
|
||||||
int(self.data['Reserve_Time_M']))
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def course(self) -> str:
|
def course(self) -> str:
|
||||||
"""Get the current course."""
|
"""Get the current course."""
|
||||||
course = lookup_reference('Course', self.data, self.dishwasher)
|
course = lookup_reference("Course", self.data, self.dishwasher)
|
||||||
if course in DISHWASHER_COURSE_MAP:
|
if course in DISHWASHER_COURSE_MAP:
|
||||||
return DISHWASHER_COURSE_MAP[course]
|
return DISHWASHER_COURSE_MAP[course]
|
||||||
else:
|
else:
|
||||||
@ -152,9 +156,9 @@ class DishWasherStatus(object):
|
|||||||
@property
|
@property
|
||||||
def smart_course(self) -> str:
|
def smart_course(self) -> str:
|
||||||
"""Get the current smart course."""
|
"""Get the current smart course."""
|
||||||
return lookup_reference('SmartCourse', self.data, self.dishwasher)
|
return lookup_reference("SmartCourse", self.data, self.dishwasher)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error(self) -> str:
|
def error(self) -> str:
|
||||||
"""Get the current error."""
|
"""Get the current error."""
|
||||||
return lookup_reference('Error', self.data, self.dishwasher)
|
return lookup_reference("Error", self.data, self.dishwasher)
|
||||||
|
129
wideq/dryer.py
129
wideq/dryer.py
@ -8,84 +8,84 @@ from .util import lookup_enum, lookup_reference
|
|||||||
class DryerState(enum.Enum):
|
class DryerState(enum.Enum):
|
||||||
"""The state of the dryer device."""
|
"""The state of the dryer device."""
|
||||||
|
|
||||||
COOLING = '@WM_STATE_COOLING_W'
|
COOLING = "@WM_STATE_COOLING_W"
|
||||||
END = '@WM_STATE_END_W'
|
END = "@WM_STATE_END_W"
|
||||||
ERROR = '@WM_STATE_ERROR_W'
|
ERROR = "@WM_STATE_ERROR_W"
|
||||||
DRYING = '@WM_STATE_DRYING_W'
|
DRYING = "@WM_STATE_DRYING_W"
|
||||||
INITIAL = '@WM_STATE_INITIAL_W'
|
INITIAL = "@WM_STATE_INITIAL_W"
|
||||||
OFF = '@WM_STATE_POWER_OFF_W'
|
OFF = "@WM_STATE_POWER_OFF_W"
|
||||||
PAUSE = '@WM_STATE_PAUSE_W'
|
PAUSE = "@WM_STATE_PAUSE_W"
|
||||||
RUNNING = '@WM_STATE_RUNNING_W'
|
RUNNING = "@WM_STATE_RUNNING_W"
|
||||||
SMART_DIAGNOSIS = '@WM_STATE_SMART_DIAGNOSIS_W'
|
SMART_DIAGNOSIS = "@WM_STATE_SMART_DIAGNOSIS_W"
|
||||||
WRINKLE_CARE = '@WM_STATE_WRINKLECARE_W'
|
WRINKLE_CARE = "@WM_STATE_WRINKLECARE_W"
|
||||||
UNKNOWN = _UNKNOWN
|
UNKNOWN = _UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
class DryLevel(enum.Enum):
|
class DryLevel(enum.Enum):
|
||||||
"""Represents the dry level setting of the dryer."""
|
"""Represents the dry level setting of the dryer."""
|
||||||
|
|
||||||
CUPBOARD = '@WM_DRY27_DRY_LEVEL_CUPBOARD_W'
|
CUPBOARD = "@WM_DRY27_DRY_LEVEL_CUPBOARD_W"
|
||||||
DAMP = '@WM_DRY27_DRY_LEVEL_DAMP_W'
|
DAMP = "@WM_DRY27_DRY_LEVEL_DAMP_W"
|
||||||
EXTRA = '@WM_DRY27_DRY_LEVEL_EXTRA_W'
|
EXTRA = "@WM_DRY27_DRY_LEVEL_EXTRA_W"
|
||||||
IRON = '@WM_DRY27_DRY_LEVEL_IRON_W'
|
IRON = "@WM_DRY27_DRY_LEVEL_IRON_W"
|
||||||
LESS = '@WM_DRY27_DRY_LEVEL_LESS_W'
|
LESS = "@WM_DRY27_DRY_LEVEL_LESS_W"
|
||||||
MORE = '@WM_DRY27_DRY_LEVEL_MORE_W'
|
MORE = "@WM_DRY27_DRY_LEVEL_MORE_W"
|
||||||
NORMAL = '@WM_DRY27_DRY_LEVEL_NORMAL_W'
|
NORMAL = "@WM_DRY27_DRY_LEVEL_NORMAL_W"
|
||||||
OFF = '-'
|
OFF = "-"
|
||||||
VERY = '@WM_DRY27_DRY_LEVEL_VERY_W'
|
VERY = "@WM_DRY27_DRY_LEVEL_VERY_W"
|
||||||
UNKNOWN = _UNKNOWN
|
UNKNOWN = _UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
class DryerError(enum.Enum):
|
class DryerError(enum.Enum):
|
||||||
"""A dryer error."""
|
"""A dryer error."""
|
||||||
|
|
||||||
ERROR_AE = '@WM_US_DRYER_ERROR_AE_W'
|
ERROR_AE = "@WM_US_DRYER_ERROR_AE_W"
|
||||||
ERROR_CE1 = '@WM_US_DRYER_ERROR_CE1_W'
|
ERROR_CE1 = "@WM_US_DRYER_ERROR_CE1_W"
|
||||||
ERROR_DE4 = '@WM_WW_FL_ERROR_DE4_W'
|
ERROR_DE4 = "@WM_WW_FL_ERROR_DE4_W"
|
||||||
ERROR_DOOR = '@WM_US_DRYER_ERROR_DE_W'
|
ERROR_DOOR = "@WM_US_DRYER_ERROR_DE_W"
|
||||||
ERROR_DRAINMOTOR = '@WM_US_DRYER_ERROR_OE_W'
|
ERROR_DRAINMOTOR = "@WM_US_DRYER_ERROR_OE_W"
|
||||||
ERROR_EMPTYWATER = '@WM_US_DRYER_ERROR_EMPTYWATER_W'
|
ERROR_EMPTYWATER = "@WM_US_DRYER_ERROR_EMPTYWATER_W"
|
||||||
ERROR_F1 = '@WM_US_DRYER_ERROR_F1_W'
|
ERROR_F1 = "@WM_US_DRYER_ERROR_F1_W"
|
||||||
ERROR_LE1 = '@WM_US_DRYER_ERROR_LE1_W'
|
ERROR_LE1 = "@WM_US_DRYER_ERROR_LE1_W"
|
||||||
ERROR_LE2 = '@WM_US_DRYER_ERROR_LE2_W'
|
ERROR_LE2 = "@WM_US_DRYER_ERROR_LE2_W"
|
||||||
ERROR_NOFILTER = '@WM_US_DRYER_ERROR_NOFILTER_W'
|
ERROR_NOFILTER = "@WM_US_DRYER_ERROR_NOFILTER_W"
|
||||||
ERROR_NP = '@WM_US_DRYER_ERROR_NP_GAS_W'
|
ERROR_NP = "@WM_US_DRYER_ERROR_NP_GAS_W"
|
||||||
ERROR_PS = '@WM_US_DRYER_ERROR_PS_W'
|
ERROR_PS = "@WM_US_DRYER_ERROR_PS_W"
|
||||||
ERROR_TE1 = '@WM_US_DRYER_ERROR_TE1_W'
|
ERROR_TE1 = "@WM_US_DRYER_ERROR_TE1_W"
|
||||||
ERROR_TE2 = '@WM_US_DRYER_ERROR_TE2_W'
|
ERROR_TE2 = "@WM_US_DRYER_ERROR_TE2_W"
|
||||||
ERROR_TE5 = '@WM_US_DRYER_ERROR_TE5_W'
|
ERROR_TE5 = "@WM_US_DRYER_ERROR_TE5_W"
|
||||||
ERROR_TE6 = '@WM_US_DRYER_ERROR_TE6_W'
|
ERROR_TE6 = "@WM_US_DRYER_ERROR_TE6_W"
|
||||||
UNKNOWN = _UNKNOWN
|
UNKNOWN = _UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
class TempControl(enum.Enum):
|
class TempControl(enum.Enum):
|
||||||
"""Represents temperature control setting."""
|
"""Represents temperature control setting."""
|
||||||
|
|
||||||
OFF = '-'
|
OFF = "-"
|
||||||
ULTRA_LOW = '@WM_DRY27_TEMP_ULTRA_LOW_W'
|
ULTRA_LOW = "@WM_DRY27_TEMP_ULTRA_LOW_W"
|
||||||
LOW = '@WM_DRY27_TEMP_LOW_W'
|
LOW = "@WM_DRY27_TEMP_LOW_W"
|
||||||
MEDIUM = '@WM_DRY27_TEMP_MEDIUM_W'
|
MEDIUM = "@WM_DRY27_TEMP_MEDIUM_W"
|
||||||
MID_HIGH = '@WM_DRY27_TEMP_MID_HIGH_W'
|
MID_HIGH = "@WM_DRY27_TEMP_MID_HIGH_W"
|
||||||
HIGH = '@WM_DRY27_TEMP_HIGH_W'
|
HIGH = "@WM_DRY27_TEMP_HIGH_W"
|
||||||
UNKNOWN = _UNKNOWN
|
UNKNOWN = _UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
class TimeDry(enum.Enum):
|
class TimeDry(enum.Enum):
|
||||||
"""Represents a timed dry setting."""
|
"""Represents a timed dry setting."""
|
||||||
|
|
||||||
OFF = '-'
|
OFF = "-"
|
||||||
TWENTY = '20'
|
TWENTY = "20"
|
||||||
THIRTY = '30'
|
THIRTY = "30"
|
||||||
FOURTY = '40'
|
FOURTY = "40"
|
||||||
FIFTY = '50'
|
FIFTY = "50"
|
||||||
SIXTY = '60'
|
SIXTY = "60"
|
||||||
UNKNOWN = _UNKNOWN
|
UNKNOWN = _UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
class DryerDevice(Device):
|
class DryerDevice(Device):
|
||||||
"""A higher-level interface for a dryer."""
|
"""A higher-level interface for a dryer."""
|
||||||
|
|
||||||
def poll(self) -> Optional['DryerStatus']:
|
def poll(self) -> Optional["DryerStatus"]:
|
||||||
"""Poll the device's current state.
|
"""Poll the device's current state.
|
||||||
|
|
||||||
Monitoring must be started first with `monitor_start`.
|
Monitoring must be started first with `monitor_start`.
|
||||||
@ -94,7 +94,7 @@ class DryerDevice(Device):
|
|||||||
not yet available.
|
not yet available.
|
||||||
"""
|
"""
|
||||||
# Abort if monitoring has not started yet.
|
# Abort if monitoring has not started yet.
|
||||||
if not hasattr(self, 'mon'):
|
if not hasattr(self, "mon"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.mon.poll()
|
data = self.mon.poll()
|
||||||
@ -121,34 +121,34 @@ class DryerStatus(object):
|
|||||||
bit_index = 2 ** index
|
bit_index = 2 ** index
|
||||||
mode = bin(bit_value & bit_index)
|
mode = bin(bit_value & bit_index)
|
||||||
if mode == bin(0):
|
if mode == bin(0):
|
||||||
return 'OFF'
|
return "OFF"
|
||||||
else:
|
else:
|
||||||
return 'ON'
|
return "ON"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> DryerState:
|
def state(self) -> DryerState:
|
||||||
"""Get the state of the dryer."""
|
"""Get the state of the dryer."""
|
||||||
return DryerState(lookup_enum('State', self.data, self.dryer))
|
return DryerState(lookup_enum("State", self.data, self.dryer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def previous_state(self) -> DryerState:
|
def previous_state(self) -> DryerState:
|
||||||
"""Get the previous state of the dryer."""
|
"""Get the previous state of the dryer."""
|
||||||
return DryerState(lookup_enum('PreState', self.data, self.dryer))
|
return DryerState(lookup_enum("PreState", self.data, self.dryer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dry_level(self) -> DryLevel:
|
def dry_level(self) -> DryLevel:
|
||||||
"""Get the dry level."""
|
"""Get the dry level."""
|
||||||
return DryLevel(lookup_enum('DryLevel', self.data, self.dryer))
|
return DryLevel(lookup_enum("DryLevel", self.data, self.dryer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temperature_control(self) -> TempControl:
|
def temperature_control(self) -> TempControl:
|
||||||
"""Get the temperature control setting."""
|
"""Get the temperature control setting."""
|
||||||
return TempControl(lookup_enum('TempControl', self.data, self.dryer))
|
return TempControl(lookup_enum("TempControl", self.data, self.dryer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_dry(self) -> TimeDry:
|
def time_dry(self) -> TimeDry:
|
||||||
"""Get the time dry setting."""
|
"""Get the time dry setting."""
|
||||||
return TimeDry(lookup_enum('TimeDry', self.data, self.dryer))
|
return TimeDry(lookup_enum("TimeDry", self.data, self.dryer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -158,27 +158,28 @@ class DryerStatus(object):
|
|||||||
@property
|
@property
|
||||||
def remaining_time(self) -> int:
|
def remaining_time(self) -> int:
|
||||||
"""Get the remaining time in minutes."""
|
"""Get the remaining time in minutes."""
|
||||||
return (int(self.data['Remain_Time_H']) * 60 +
|
return int(self.data["Remain_Time_H"]) * 60 + int(
|
||||||
int(self.data['Remain_Time_M']))
|
self.data["Remain_Time_M"]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_time(self) -> int:
|
def initial_time(self) -> int:
|
||||||
"""Get the initial time in minutes."""
|
"""Get the initial time in minutes."""
|
||||||
return (
|
return int(self.data["Initial_Time_H"]) * 60 + int(
|
||||||
int(self.data['Initial_Time_H']) * 60 +
|
self.data["Initial_Time_M"]
|
||||||
int(self.data['Initial_Time_M']))
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def course(self) -> str:
|
def course(self) -> str:
|
||||||
"""Get the current course."""
|
"""Get the current course."""
|
||||||
return lookup_reference('Course', self.data, self.dryer)
|
return lookup_reference("Course", self.data, self.dryer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def smart_course(self) -> str:
|
def smart_course(self) -> str:
|
||||||
"""Get the current smart course."""
|
"""Get the current smart course."""
|
||||||
return lookup_reference('SmartCourse', self.data, self.dryer)
|
return lookup_reference("SmartCourse", self.data, self.dryer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error(self) -> str:
|
def error(self) -> str:
|
||||||
"""Get the current error."""
|
"""Get the current error."""
|
||||||
return lookup_reference('Error', self.data, self.dryer)
|
return lookup_reference("Error", self.data, self.dryer)
|
||||||
|
@ -38,18 +38,16 @@ class RefrigeratorDevice(Device):
|
|||||||
"""A higher-level interface for a refrigerator."""
|
"""A higher-level interface for a refrigerator."""
|
||||||
|
|
||||||
def set_temp_refrigerator_c(self, temp):
|
def set_temp_refrigerator_c(self, temp):
|
||||||
"""Set the refrigerator temperature in Celsius.
|
"""Set the refrigerator temperature in Celsius."""
|
||||||
"""
|
value = self.model.enum_value("TempRefrigerator", str(temp))
|
||||||
value = self.model.enum_value('TempRefrigerator', str(temp))
|
self._set_control("RETM", value)
|
||||||
self._set_control('RETM', value)
|
|
||||||
|
|
||||||
def set_temp_freezer_c(self, temp):
|
def set_temp_freezer_c(self, temp):
|
||||||
"""Set the freezer temperature in Celsius.
|
"""Set the freezer temperature in Celsius."""
|
||||||
"""
|
value = self.model.enum_value("TempFreezer", str(temp))
|
||||||
value = self.model.enum_value('TempFreezer', str(temp))
|
self._set_control("REFT", value)
|
||||||
self._set_control('REFT', value)
|
|
||||||
|
|
||||||
def poll(self) -> Optional['RefrigeratorStatus']:
|
def poll(self) -> Optional["RefrigeratorStatus"]:
|
||||||
"""Poll the device's current state.
|
"""Poll the device's current state.
|
||||||
|
|
||||||
Monitoring must be started first with `monitor_start`.
|
Monitoring must be started first with `monitor_start`.
|
||||||
@ -58,7 +56,7 @@ class RefrigeratorDevice(Device):
|
|||||||
status is not yet available.
|
status is not yet available.
|
||||||
"""
|
"""
|
||||||
# Abort if monitoring has not started yet.
|
# Abort if monitoring has not started yet.
|
||||||
if not hasattr(self, 'mon'):
|
if not hasattr(self, "mon"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.mon.poll()
|
data = self.mon.poll()
|
||||||
@ -82,59 +80,59 @@ class RefrigeratorStatus(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_refrigerator_c(self):
|
def temp_refrigerator_c(self):
|
||||||
temp = lookup_enum('TempRefrigerator', self.data, self.refrigerator)
|
temp = lookup_enum("TempRefrigerator", self.data, self.refrigerator)
|
||||||
return int(temp)
|
return int(temp)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_freezer_c(self):
|
def temp_freezer_c(self):
|
||||||
temp = lookup_enum('TempFreezer', self.data, self.refrigerator)
|
temp = lookup_enum("TempFreezer", self.data, self.refrigerator)
|
||||||
return int(temp)
|
return int(temp)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ice_plus_status(self):
|
def ice_plus_status(self):
|
||||||
status = lookup_enum('IcePlus', self.data, self.refrigerator)
|
status = lookup_enum("IcePlus", self.data, self.refrigerator)
|
||||||
return IcePlus(status)
|
return IcePlus(status)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fresh_air_filter_status(self):
|
def fresh_air_filter_status(self):
|
||||||
status = lookup_enum('FreshAirFilter', self.data, self.refrigerator)
|
status = lookup_enum("FreshAirFilter", self.data, self.refrigerator)
|
||||||
return FreshAirFilter(status)
|
return FreshAirFilter(status)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def energy_saving_mode(self):
|
def energy_saving_mode(self):
|
||||||
mode = lookup_enum('SmartSavingMode', self.data, self.refrigerator)
|
mode = lookup_enum("SmartSavingMode", self.data, self.refrigerator)
|
||||||
return SmartSavingMode(mode)
|
return SmartSavingMode(mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def door_opened(self):
|
def door_opened(self):
|
||||||
state = lookup_enum('DoorOpenState', self.data, self.refrigerator)
|
state = lookup_enum("DoorOpenState", self.data, self.refrigerator)
|
||||||
return state == "OPEN"
|
return state == "OPEN"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def temp_unit(self):
|
def temp_unit(self):
|
||||||
return lookup_enum('TempUnit', self.data, self.refrigerator)
|
return lookup_enum("TempUnit", self.data, self.refrigerator)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def energy_saving_enabled(self):
|
def energy_saving_enabled(self):
|
||||||
mode = lookup_enum(
|
mode = lookup_enum(
|
||||||
'SmartSavingModeStatus', self.data, self.refrigerator
|
"SmartSavingModeStatus", self.data, self.refrigerator
|
||||||
)
|
)
|
||||||
return mode == 'ON'
|
return mode == "ON"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locked(self):
|
def locked(self):
|
||||||
status = lookup_enum('LockingStatus', self.data, self.refrigerator)
|
status = lookup_enum("LockingStatus", self.data, self.refrigerator)
|
||||||
return status == "LOCK"
|
return status == "LOCK"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_saving_status(self):
|
def active_saving_status(self):
|
||||||
return self.data['ActiveSavingStatus']
|
return self.data["ActiveSavingStatus"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def eco_enabled(self):
|
def eco_enabled(self):
|
||||||
eco = lookup_enum('EcoFriendly', self.data, self.refrigerator)
|
eco = lookup_enum("EcoFriendly", self.data, self.refrigerator)
|
||||||
return eco == "@CP_ON_EN_W"
|
return eco == "@CP_ON_EN_W"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def water_filter_used_month(self):
|
def water_filter_used_month(self):
|
||||||
return self.data['WaterFilterUsedMonth']
|
return self.data["WaterFilterUsedMonth"]
|
||||||
|
@ -3,7 +3,7 @@ from typing import TypeVar
|
|||||||
from .client import Device, DeviceType
|
from .client import Device, DeviceType
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar('T', bound=Device)
|
T = TypeVar("T", bound=Device)
|
||||||
|
|
||||||
|
|
||||||
def lookup_enum(attr: str, data: dict, device: T):
|
def lookup_enum(attr: str, data: dict, device: T):
|
||||||
@ -27,13 +27,12 @@ def lookup_reference(attr: str, data: dict, device: T) -> str:
|
|||||||
"""
|
"""
|
||||||
value = device.model.reference_name(attr, data[attr])
|
value = device.model.reference_name(attr, data[attr])
|
||||||
if value is None:
|
if value is None:
|
||||||
return 'Off'
|
return "Off"
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def device_classes():
|
def device_classes():
|
||||||
"""The mapping of every Device subclass related to the DeviceType enum
|
"""The mapping of every Device subclass related to the DeviceType enum"""
|
||||||
"""
|
|
||||||
from .ac import ACDevice
|
from .ac import ACDevice
|
||||||
from .dryer import DryerDevice
|
from .dryer import DryerDevice
|
||||||
from .dishwasher import DishWasherDevice
|
from .dishwasher import DishWasherDevice
|
||||||
|
@ -8,36 +8,36 @@ from .util import lookup_enum, lookup_reference
|
|||||||
class WasherState(enum.Enum):
|
class WasherState(enum.Enum):
|
||||||
"""The state of the washer device."""
|
"""The state of the washer device."""
|
||||||
|
|
||||||
ADD_DRAIN = '@WM_STATE_ADD_DRAIN_W'
|
ADD_DRAIN = "@WM_STATE_ADD_DRAIN_W"
|
||||||
COMPLETE = '@WM_STATE_COMPLETE_W'
|
COMPLETE = "@WM_STATE_COMPLETE_W"
|
||||||
DETECTING = '@WM_STATE_DETECTING_W'
|
DETECTING = "@WM_STATE_DETECTING_W"
|
||||||
DETERGENT_AMOUNT = '@WM_STATE_DETERGENT_AMOUNT_W'
|
DETERGENT_AMOUNT = "@WM_STATE_DETERGENT_AMOUNT_W"
|
||||||
DRYING = '@WM_STATE_DRYING_W'
|
DRYING = "@WM_STATE_DRYING_W"
|
||||||
END = '@WM_STATE_END_W'
|
END = "@WM_STATE_END_W"
|
||||||
ERROR_AUTO_OFF = '@WM_STATE_ERROR_AUTO_OFF_W'
|
ERROR_AUTO_OFF = "@WM_STATE_ERROR_AUTO_OFF_W"
|
||||||
FRESH_CARE = '@WM_STATE_FRESHCARE_W'
|
FRESH_CARE = "@WM_STATE_FRESHCARE_W"
|
||||||
FROZEN_PREVENT_INITIAL = '@WM_STATE_FROZEN_PREVENT_INITIAL_W'
|
FROZEN_PREVENT_INITIAL = "@WM_STATE_FROZEN_PREVENT_INITIAL_W"
|
||||||
FROZEN_PREVENT_PAUSE = '@WM_STATE_FROZEN_PREVENT_PAUSE_W'
|
FROZEN_PREVENT_PAUSE = "@WM_STATE_FROZEN_PREVENT_PAUSE_W"
|
||||||
FROZEN_PREVENT_RUNNING = '@WM_STATE_FROZEN_PREVENT_RUNNING_W'
|
FROZEN_PREVENT_RUNNING = "@WM_STATE_FROZEN_PREVENT_RUNNING_W"
|
||||||
INITIAL = '@WM_STATE_INITIAL_W'
|
INITIAL = "@WM_STATE_INITIAL_W"
|
||||||
OFF = '@WM_STATE_POWER_OFF_W'
|
OFF = "@WM_STATE_POWER_OFF_W"
|
||||||
PAUSE = '@WM_STATE_PAUSE_W'
|
PAUSE = "@WM_STATE_PAUSE_W"
|
||||||
PRE_WASH = '@WM_STATE_PREWASH_W'
|
PRE_WASH = "@WM_STATE_PREWASH_W"
|
||||||
RESERVE = '@WM_STATE_RESERVE_W'
|
RESERVE = "@WM_STATE_RESERVE_W"
|
||||||
RINSING = '@WM_STATE_RINSING_W'
|
RINSING = "@WM_STATE_RINSING_W"
|
||||||
RINSE_HOLD = '@WM_STATE_RINSE_HOLD_W'
|
RINSE_HOLD = "@WM_STATE_RINSE_HOLD_W"
|
||||||
RUNNING = '@WM_STATE_RUNNING_W'
|
RUNNING = "@WM_STATE_RUNNING_W"
|
||||||
SMART_DIAGNOSIS = '@WM_STATE_SMART_DIAG_W'
|
SMART_DIAGNOSIS = "@WM_STATE_SMART_DIAG_W"
|
||||||
SMART_DIAGNOSIS_DATA = '@WM_STATE_SMART_DIAGDATA_W'
|
SMART_DIAGNOSIS_DATA = "@WM_STATE_SMART_DIAGDATA_W"
|
||||||
SPINNING = '@WM_STATE_SPINNING_W'
|
SPINNING = "@WM_STATE_SPINNING_W"
|
||||||
TCL_ALARM_NORMAL = 'TCL_ALARM_NORMAL'
|
TCL_ALARM_NORMAL = "TCL_ALARM_NORMAL"
|
||||||
TUBCLEAN_COUNT_ALARM = '@WM_STATE_TUBCLEAN_COUNT_ALRAM_W'
|
TUBCLEAN_COUNT_ALARM = "@WM_STATE_TUBCLEAN_COUNT_ALRAM_W"
|
||||||
|
|
||||||
|
|
||||||
class WasherDevice(Device):
|
class WasherDevice(Device):
|
||||||
"""A higher-level interface for a washer."""
|
"""A higher-level interface for a washer."""
|
||||||
|
|
||||||
def poll(self) -> Optional['WasherStatus']:
|
def poll(self) -> Optional["WasherStatus"]:
|
||||||
"""Poll the device's current state.
|
"""Poll the device's current state.
|
||||||
|
|
||||||
Monitoring must be started first with `monitor_start`.
|
Monitoring must be started first with `monitor_start`.
|
||||||
@ -46,7 +46,7 @@ class WasherDevice(Device):
|
|||||||
not yet available.
|
not yet available.
|
||||||
"""
|
"""
|
||||||
# Abort if monitoring has not started yet.
|
# Abort if monitoring has not started yet.
|
||||||
if not hasattr(self, 'mon'):
|
if not hasattr(self, "mon"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
data = self.mon.poll()
|
data = self.mon.poll()
|
||||||
@ -71,12 +71,12 @@ class WasherStatus(object):
|
|||||||
@property
|
@property
|
||||||
def state(self) -> WasherState:
|
def state(self) -> WasherState:
|
||||||
"""Get the state of the washer."""
|
"""Get the state of the washer."""
|
||||||
return WasherState(lookup_enum('State', self.data, self.washer))
|
return WasherState(lookup_enum("State", self.data, self.washer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def previous_state(self) -> WasherState:
|
def previous_state(self) -> WasherState:
|
||||||
"""Get the previous state of the washer."""
|
"""Get the previous state of the washer."""
|
||||||
return WasherState(lookup_enum('PreState', self.data, self.washer))
|
return WasherState(lookup_enum("PreState", self.data, self.washer))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -86,15 +86,16 @@ class WasherStatus(object):
|
|||||||
@property
|
@property
|
||||||
def remaining_time(self) -> int:
|
def remaining_time(self) -> int:
|
||||||
"""Get the remaining time in minutes."""
|
"""Get the remaining time in minutes."""
|
||||||
return (int(self.data['Remain_Time_H']) * 60 +
|
return int(self.data["Remain_Time_H"]) * 60 + int(
|
||||||
int(self.data['Remain_Time_M']))
|
self.data["Remain_Time_M"]
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def initial_time(self) -> int:
|
def initial_time(self) -> int:
|
||||||
"""Get the initial time in minutes."""
|
"""Get the initial time in minutes."""
|
||||||
return (
|
return int(self.data["Initial_Time_H"]) * 60 + int(
|
||||||
int(self.data['Initial_Time_H']) * 60 +
|
self.data["Initial_Time_M"]
|
||||||
int(self.data['Initial_Time_M']))
|
)
|
||||||
|
|
||||||
def _lookup_reference(self, attr: str) -> str:
|
def _lookup_reference(self, attr: str) -> str:
|
||||||
"""Look up a reference value for the provided attribute.
|
"""Look up a reference value for the provided attribute.
|
||||||
@ -104,20 +105,20 @@ class WasherStatus(object):
|
|||||||
"""
|
"""
|
||||||
value = self.washer.model.reference_name(attr, self.data[attr])
|
value = self.washer.model.reference_name(attr, self.data[attr])
|
||||||
if value is None:
|
if value is None:
|
||||||
return 'Off'
|
return "Off"
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def course(self) -> str:
|
def course(self) -> str:
|
||||||
"""Get the current course."""
|
"""Get the current course."""
|
||||||
return lookup_reference('APCourse', self.data, self.washer)
|
return lookup_reference("APCourse", self.data, self.washer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def smart_course(self) -> str:
|
def smart_course(self) -> str:
|
||||||
"""Get the current smart course."""
|
"""Get the current smart course."""
|
||||||
return lookup_reference('SmartCourse', self.data, self.washer)
|
return lookup_reference("SmartCourse", self.data, self.washer)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error(self) -> str:
|
def error(self) -> str:
|
||||||
"""Get the current error."""
|
"""Get the current error."""
|
||||||
return lookup_reference('Error', self.data, self.washer)
|
return lookup_reference("Error", self.data, self.washer)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user