mirror of
https://github.com/no2chem/wideq.git
synced 2025-05-15 23:00:18 -07:00
This fixes #83 - I have the same LP1419IVSM Air Conditioner model, and it causes an error when turning on using wideq. I've updated the ACDevice class with two new @properties, which are the list of Operations the device supports, and the best-supported ON operation from that list. This makes the presumption that if the model reports ALL_ON as available, that it is appropriate to use (as the existing code did), but if ALL_ON is not supported by the particular model, it will fall back to using whichever ON Operation it reports as supporting. In the case of the LP1419, that will be RIGHT_ON. Additionally, as a backstop, this will raise an error if ALL_ON is not supported but there are still more than one supported ON operation - from reading issues I haven't been able to find any evidence that such a device exists, but if one should pop up it'll likely need handling in some creative way. These new values are now also reported in the ac-config subcommand of example.py. Finally, this updates the set_on method to use the resultant operation name. This now means that example.py set [id] on works for my air conditioner again, and just in time for the warmer weather!
272 lines
7.7 KiB
Python
Executable File
272 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import wideq
|
|
import json
|
|
import time
|
|
import argparse
|
|
import sys
|
|
import re
|
|
import os.path
|
|
import logging
|
|
from typing import List
|
|
|
|
STATE_FILE = 'wideq_state.json'
|
|
LOGGER = logging.getLogger("wideq.example")
|
|
|
|
|
|
def authenticate(gateway):
|
|
"""Interactively authenticate the user via a browser to get an OAuth
|
|
session.
|
|
"""
|
|
|
|
login_url = gateway.oauth_url()
|
|
print('Log in here:')
|
|
print(login_url)
|
|
print('Then paste the URL where the browser is redirected:')
|
|
callback_url = input()
|
|
return wideq.Auth.from_url(gateway, callback_url)
|
|
|
|
|
|
def ls(client):
|
|
"""List the user's devices."""
|
|
|
|
for device in client.devices:
|
|
print('{0.id}: {0.name} ({0.type.name} {0.model_id})'.format(device))
|
|
|
|
|
|
def mon(client, device_id):
|
|
"""Monitor any device, displaying generic information about its
|
|
status.
|
|
"""
|
|
|
|
device = client.get_device(device_id)
|
|
model = client.model_info(device)
|
|
|
|
with wideq.Monitor(client.session, device_id) as mon:
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
print('Polling...')
|
|
data = mon.poll()
|
|
if data:
|
|
try:
|
|
res = model.decode_monitor(data)
|
|
except ValueError:
|
|
print('status data: {!r}'.format(data))
|
|
else:
|
|
for key, value in res.items():
|
|
try:
|
|
desc = model.value(key)
|
|
except KeyError:
|
|
print('- {}: {}'.format(key, value))
|
|
if isinstance(desc, wideq.EnumValue):
|
|
print('- {}: {}'.format(
|
|
key, desc.options.get(value, value)
|
|
))
|
|
elif isinstance(desc, wideq.RangeValue):
|
|
print('- {0}: {1} ({2.min}-{2.max})'.format(
|
|
key, value, desc,
|
|
))
|
|
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
|
|
def ac_mon(client, device_id):
|
|
"""Monitor an AC/HVAC device, showing higher-level information about
|
|
its status such as its temperature and operation mode.
|
|
"""
|
|
|
|
device = client.get_device(device_id)
|
|
if device.type != wideq.DeviceType.AC:
|
|
print('This is not an AC device.')
|
|
return
|
|
|
|
ac = wideq.ACDevice(client, device)
|
|
|
|
try:
|
|
ac.monitor_start()
|
|
except wideq.core.NotConnectedError:
|
|
print('Device not available.')
|
|
return
|
|
|
|
try:
|
|
while True:
|
|
time.sleep(1)
|
|
state = ac.poll()
|
|
if state:
|
|
print(
|
|
'{1}; '
|
|
'{0.mode.name}; '
|
|
'cur {0.temp_cur_f}°F; '
|
|
'cfg {0.temp_cfg_f}°F; '
|
|
'fan speed {0.fan_speed.name}'
|
|
.format(
|
|
state,
|
|
'on' if state.is_on else 'off'
|
|
)
|
|
)
|
|
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
ac.monitor_stop()
|
|
|
|
|
|
class UserError(Exception):
|
|
"""A user-visible command-line error.
|
|
"""
|
|
def __init__(self, msg):
|
|
self.msg = msg
|
|
|
|
|
|
def _force_device(client, device_id):
|
|
"""Look up a device in the client (using `get_device`), but raise
|
|
UserError if the device is not found.
|
|
"""
|
|
device = client.get_device(device_id)
|
|
if not device:
|
|
raise UserError('device "{}" not found'.format(device_id))
|
|
return device
|
|
|
|
|
|
def set_temp(client, device_id, temp):
|
|
"""Set the configured temperature for an AC device."""
|
|
|
|
ac = wideq.ACDevice(client, _force_device(client, device_id))
|
|
ac.set_fahrenheit(int(temp))
|
|
|
|
|
|
def turn(client, device_id, on_off):
|
|
"""Turn on/off an AC device."""
|
|
|
|
ac = wideq.ACDevice(client, _force_device(client, device_id))
|
|
ac.set_on(on_off == 'on')
|
|
|
|
|
|
def ac_config(client, device_id):
|
|
ac = wideq.ACDevice(client, _force_device(client, device_id))
|
|
print(ac.supported_operations)
|
|
print(ac.supported_on_operation)
|
|
print(ac.get_filter_state())
|
|
print(ac.get_mfilter_state())
|
|
print(ac.get_energy_target())
|
|
print(ac.get_power(), " watts")
|
|
print(ac.get_outdoor_power(), " watts")
|
|
print(ac.get_volume())
|
|
print(ac.get_light())
|
|
print(ac.get_zones())
|
|
|
|
|
|
EXAMPLE_COMMANDS = {
|
|
'ls': ls,
|
|
'mon': mon,
|
|
'ac-mon': ac_mon,
|
|
'set-temp': set_temp,
|
|
'turn': turn,
|
|
'ac-config': ac_config,
|
|
}
|
|
|
|
|
|
def example_command(client, cmd, args):
|
|
func = EXAMPLE_COMMANDS.get(cmd)
|
|
if not func:
|
|
LOGGER.error("Invalid command: '%s'.\n"
|
|
"Use one of: %s", cmd, ', '.join(EXAMPLE_COMMANDS))
|
|
return
|
|
func(client, *args)
|
|
|
|
|
|
def example(country: str, language: str, verbose: bool,
|
|
cmd: str, args: List[str]) -> None:
|
|
if verbose:
|
|
wideq.set_log_level(logging.DEBUG)
|
|
|
|
# Load the current state for the example.
|
|
try:
|
|
with open(STATE_FILE) as f:
|
|
LOGGER.debug("State file found '%s'", os.path.abspath(STATE_FILE))
|
|
state = json.load(f)
|
|
except IOError:
|
|
state = {}
|
|
LOGGER.debug("No state file found (tried: '%s')",
|
|
os.path.abspath(STATE_FILE))
|
|
|
|
client = wideq.Client.load(state)
|
|
if country:
|
|
client._country = country
|
|
if language:
|
|
client._language = language
|
|
|
|
# Log in, if we don't already have an authentication.
|
|
if not client._auth:
|
|
client._auth = authenticate(client.gateway)
|
|
|
|
# Loop to retry if session has expired.
|
|
while True:
|
|
try:
|
|
example_command(client, cmd, args)
|
|
break
|
|
|
|
except wideq.NotLoggedInError:
|
|
LOGGER.info('Session expired.')
|
|
client.refresh()
|
|
|
|
except UserError as exc:
|
|
LOGGER.error(exc.msg)
|
|
sys.exit(1)
|
|
|
|
# Save the updated state.
|
|
state = client.dump()
|
|
with open(STATE_FILE, 'w') as f:
|
|
json.dump(state, f)
|
|
LOGGER.debug("Wrote state file '%s'", os.path.abspath(STATE_FILE))
|
|
|
|
|
|
def main() -> None:
|
|
"""The main command-line entry point.
|
|
"""
|
|
parser = argparse.ArgumentParser(
|
|
description='Interact with the LG SmartThinQ API.'
|
|
)
|
|
parser.add_argument('cmd', metavar='CMD', nargs='?', default='ls',
|
|
help=f'one of: {", ".join(EXAMPLE_COMMANDS)}')
|
|
parser.add_argument('args', metavar='ARGS', nargs='*',
|
|
help='subcommand arguments')
|
|
|
|
parser.add_argument(
|
|
'--country', '-c',
|
|
help=f'country code for account (default: {wideq.DEFAULT_COUNTRY})',
|
|
default=wideq.DEFAULT_COUNTRY
|
|
)
|
|
parser.add_argument(
|
|
'--language', '-l',
|
|
help=f'language code for the API (default: {wideq.DEFAULT_LANGUAGE})',
|
|
default=wideq.DEFAULT_LANGUAGE
|
|
)
|
|
parser.add_argument(
|
|
'--verbose', '-v',
|
|
help='verbose mode to help debugging',
|
|
action='store_true', default=False
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
country_regex = re.compile(r"^[A-Z]{2,3}$")
|
|
if not country_regex.match(args.country):
|
|
LOGGER.error("Country must be two or three letters"
|
|
" all upper case (e.g. US, NO, KR) got: '%s'",
|
|
args.country)
|
|
exit(1)
|
|
language_regex = re.compile(r"^[a-z]{2,3}-[A-Z]{2,3}$")
|
|
if not language_regex.match(args.language):
|
|
LOGGER.error("Language must be a combination of language"
|
|
" and country (e.g. en-US, no-NO, kr-KR)"
|
|
" got: '%s'",
|
|
args.language)
|
|
exit(1)
|
|
example(args.country, args.language, args.verbose, args.cmd, args.args)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|