1
0
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:
Michael Wei 2020-11-21 19:46:57 +00:00
parent 6e4f9d6484
commit fb3c755c2e
18 changed files with 807 additions and 695 deletions

5
.flake8 Normal file
View 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
View 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

View File

@ -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
------- -------

View File

@ -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
)/
'''

View File

@ -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")

View File

@ -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")

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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):

View File

@ -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):

View File

@ -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"]

View File

@ -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)

View File

@ -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)

View File

@ -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"]

View File

@ -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

View File

@ -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)