Const and logging cleanup, addition of is_cleaning and is_charging

All sucks vocabulary is now defined up-front in consts, so that library users can import those const instead of hard-coding them.

is_cleaning and is_charging properties were added to VacBot, avoiding library users to have to understand which vacuum statuses mean active cleaning.

All logging has been moved to instantiated loggers using the module's namespace, as is recommended by the Python docs.
This commit is contained in:
Greg Laabs
2018-07-19 12:18:22 -07:00
parent 4c1d68f924
commit 930e475905
3 changed files with 140 additions and 68 deletions
+98 -62
View File
@@ -11,58 +11,86 @@ from sleekxmpp import ClientXMPP, Callback, MatchXPath
from sleekxmpp.xmlstream import ET from sleekxmpp.xmlstream import ET
from sleekxmpp.exceptions import XMPPError from sleekxmpp.exceptions import XMPPError
# These consts convert to and from Sucks's consts (which closely match what the UI and manuals use) _LOGGER = logging.getLogger(__name__)
# These consts define all of the vocabulary used by this library when presenting various states and components.
# Applications implementing this library should import these rather than hard-code the strings, for future-proofing.
CLEAN_MODE_AUTO = 'auto'
CLEAN_MODE_EDGE = 'edge'
CLEAN_MODE_SPOT = 'spot'
CLEAN_MODE_SINGLE_ROOM = 'single_room'
CLEAN_MODE_STOP = 'stop'
FAN_SPEED_NORMAL = 'normal'
FAN_SPEED_HIGH = 'high'
CHARGE_MODE_RETURN = 'return'
CHARGE_MODE_RETURNING = 'returning'
CHARGE_MODE_CHARGING = 'charging'
CHARGE_MODE_IDLE = 'idle'
COMPONENT_SIDE_BRUSH = 'side_brush'
COMPONENT_MAIN_BRUSH = 'main_brush'
COMPONENT_FILTER = 'filter'
VACUUM_STATUS_OFFLINE = 'offline'
CLEANING_STATES = {CLEAN_MODE_AUTO, CLEAN_MODE_EDGE, CLEAN_MODE_SPOT, CLEAN_MODE_SINGLE_ROOM}
CHARGING_STATES = {CHARGE_MODE_CHARGING}
# These dictionaries convert to and from Sucks's consts (which closely match what the UI and manuals use)
# to and from what the Ecovacs API uses (which are sometimes very oddly named and have random capitalization.) # to and from what the Ecovacs API uses (which are sometimes very oddly named and have random capitalization.)
CLEAN_MODE_TO_ECOVACS = { CLEAN_MODE_TO_ECOVACS = {
'auto': 'auto', CLEAN_MODE_AUTO: 'auto',
'edge': 'border', CLEAN_MODE_EDGE: 'border',
'spot': 'spot', CLEAN_MODE_SPOT: 'spot',
'single_room': 'singleroom', CLEAN_MODE_SINGLE_ROOM: 'singleroom',
'stop': 'stop' CLEAN_MODE_STOP: 'stop'
} }
CLEAN_MODE_FROM_ECOVACS = { CLEAN_MODE_FROM_ECOVACS = {
'auto': 'auto', 'auto': CLEAN_MODE_AUTO,
'border': 'edge', 'border': CLEAN_MODE_EDGE,
'spot': 'spot', 'spot': CLEAN_MODE_SPOT,
'singleroom': 'single_room', 'singleroom': CLEAN_MODE_SINGLE_ROOM,
'stop': 'stop', 'stop': CLEAN_MODE_STOP,
'going': 'returning' 'going': CHARGE_MODE_RETURNING
} }
FAN_SPEED_TO_ECOVACS = { FAN_SPEED_TO_ECOVACS = {
'normal': 'standard', FAN_SPEED_NORMAL: 'standard',
'high': 'strong' FAN_SPEED_HIGH: 'strong'
} }
FAN_SPEED_FROM_ECOVACS = { FAN_SPEED_FROM_ECOVACS = {
'standard': 'normal', 'standard': FAN_SPEED_NORMAL,
'strong': 'high' 'strong': FAN_SPEED_HIGH
} }
CHARGE_MODE_TO_ECOVACS = { CHARGE_MODE_TO_ECOVACS = {
'return': 'go', CHARGE_MODE_RETURN: 'go',
'returning': 'Going', CHARGE_MODE_RETURNING: 'Going',
'charging': 'SlotCharging', CHARGE_MODE_CHARGING: 'SlotCharging',
'idle': 'Idle' CHARGE_MODE_IDLE: 'Idle'
} }
CHARGE_MODE_FROM_ECOVACS = { CHARGE_MODE_FROM_ECOVACS = {
'going': 'returning', 'going': CHARGE_MODE_RETURNING,
'slot_charging': 'charging', 'slot_charging': CHARGE_MODE_CHARGING,
'idle': 'idle' 'idle': CHARGE_MODE_IDLE
} }
COMPONENT_TO_ECOVACS = { COMPONENT_TO_ECOVACS = {
'main_brush': 'Brush', COMPONENT_MAIN_BRUSH: 'Brush',
'side_brush': 'SideBrush', COMPONENT_SIDE_BRUSH: 'SideBrush',
'filter': 'DustCaseHeap' COMPONENT_FILTER: 'DustCaseHeap'
} }
COMPONENT_FROM_ECOVACS = { COMPONENT_FROM_ECOVACS = {
'brush': 'main_brush', 'brush': COMPONENT_MAIN_BRUSH,
'side_brush': 'side_brush', 'side_brush': COMPONENT_SIDE_BRUSH,
'dust_case_heap': 'filter' 'dust_case_heap': COMPONENT_FILTER
} }
class EcoVacsAPI: class EcoVacsAPI:
@@ -83,7 +111,7 @@ class EcoVacsAPI:
'channel': 'c_googleplay', 'channel': 'c_googleplay',
'deviceType': '1' 'deviceType': '1'
} }
logging.debug("Setting up EcoVacsAPI") _LOGGER.debug("Setting up EcoVacsAPI")
self.resource = device_id[0:8] self.resource = device_id[0:8]
self.country = country self.country = country
self.continent = continent self.continent = continent
@@ -96,7 +124,7 @@ class EcoVacsAPI:
('uid', self.uid), ('uid', self.uid),
('accessToken', self.login_access_token))['authCode'] ('accessToken', self.login_access_token))['authCode']
self.user_access_token = self.__call_login_by_it_token()['token'] self.user_access_token = self.__call_login_by_it_token()['token']
logging.debug("EcoVacsAPI connection complete") _LOGGER.debug("EcoVacsAPI connection complete")
def __sign(self, params): def __sign(self, params):
result = params.copy() result = params.copy()
@@ -113,34 +141,34 @@ class EcoVacsAPI:
return result return result
def __call_main_api(self, function, *args): def __call_main_api(self, function, *args):
logging.debug("calling main api {} with {}".format(function, args)) _LOGGER.debug("calling main api {} with {}".format(function, args))
params = OrderedDict(args) params = OrderedDict(args)
params['requestId'] = self.md5(time.time()) params['requestId'] = self.md5(time.time())
url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta) url = (EcoVacsAPI.MAIN_URL_FORMAT + "/" + function).format(**self.meta)
api_response = requests.get(url, self.__sign(params)) api_response = requests.get(url, self.__sign(params))
json = api_response.json() json = api_response.json()
logging.debug("got {}".format(json)) _LOGGER.debug("got {}".format(json))
if json['code'] == '0000': if json['code'] == '0000':
return json['data'] return json['data']
elif json['code'] == '1005': elif json['code'] == '1005':
logging.warning("incorrect email or password") _LOGGER.warning("incorrect email or password")
raise ValueError("incorrect email or password") raise ValueError("incorrect email or password")
else: else:
logging.error("call to {} failed with {}".format(function, json)) _LOGGER.error("call to {} failed with {}".format(function, json))
raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format( raise RuntimeError("failure code {} ({}) for call {} and parameters {}".format(
json['code'], json['msg'], function, args)) json['code'], json['msg'], function, args))
def __call_user_api(self, function, args): def __call_user_api(self, function, args):
logging.debug("calling user api {} with {}".format(function, args)) _LOGGER.debug("calling user api {} with {}".format(function, args))
params = {'todo': function} params = {'todo': function}
params.update(args) params.update(args)
response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params) response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params)
json = response.json() json = response.json()
logging.debug("got {}".format(json)) _LOGGER.debug("got {}".format(json))
if json['result'] == 'ok': if json['result'] == 'ok':
return json return json
else: else:
logging.error("call to {} failed with {}".format(function, json)) _LOGGER.error("call to {} failed with {}".format(function, json))
raise RuntimeError( raise RuntimeError(
"failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params)) "failure {} ({}) for call {} and parameters {}".format(json['error'], json['errno'], function, params))
@@ -259,14 +287,14 @@ class VacBot():
def _handle_error(self, event): def _handle_error(self, event):
error = event['error'] error = event['error']
self.errorEvents.notify(error) self.errorEvents.notify(error)
logging.debug("*** error = " + error) _LOGGER.debug("*** error = " + error)
def _handle_life_span(self, event): def _handle_life_span(self, event):
type = event['type'] type = event['type']
try: try:
type = COMPONENT_FROM_ECOVACS[type] type = COMPONENT_FROM_ECOVACS[type]
except KeyError: except KeyError:
logging.warning("Unknown component type: '" + type + "'") _LOGGER.warning("Unknown component type: '" + type + "'")
total = float(event['total']) total = float(event['total'])
val = float(event['val']) val = float(event['val'])
@@ -275,14 +303,14 @@ class VacBot():
lifespan_event = {'type': type, 'lifespan': lifespan} lifespan_event = {'type': type, 'lifespan': lifespan}
self.lifespanEvents.notify(lifespan_event) self.lifespanEvents.notify(lifespan_event)
logging.debug("*** life_span " + type + " = " + str(lifespan)) _LOGGER.debug("*** life_span " + type + " = " + str(lifespan))
def _handle_clean_report(self, event): def _handle_clean_report(self, event):
type = event['type'] type = event['type']
try: try:
type = CLEAN_MODE_FROM_ECOVACS[type] type = CLEAN_MODE_FROM_ECOVACS[type]
except KeyError: except KeyError:
logging.warning("Unknown cleaning status '" + type + "'") _LOGGER.warning("Unknown cleaning status '" + type + "'")
self.clean_status = type self.clean_status = type
self.vacuum_status = type self.vacuum_status = type
fan = event.get('speed', None) fan = event.get('speed', None)
@@ -290,29 +318,29 @@ class VacBot():
try: try:
fan = FAN_SPEED_FROM_ECOVACS[fan] fan = FAN_SPEED_FROM_ECOVACS[fan]
except KeyError: except KeyError:
logging.warning("Unknown fan speed: '" + fan + "'") _LOGGER.warning("Unknown fan speed: '" + fan + "'")
self.fan_speed = fan self.fan_speed = fan
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
if self.fan_speed: if self.fan_speed:
logging.debug("*** clean_status = " + self.clean_status + " fan_speed = " + self.fan_speed) _LOGGER.debug("*** clean_status = " + self.clean_status + " fan_speed = " + self.fan_speed)
else: else:
logging.debug("*** clean_status = " + self.clean_status + " fan_speed = None") _LOGGER.debug("*** clean_status = " + self.clean_status + " fan_speed = None")
def _handle_battery_info(self, iq): def _handle_battery_info(self, iq):
try: try:
self.battery_status = float(iq['power']) / 100 self.battery_status = float(iq['power']) / 100
except ValueError: except ValueError:
logging.warning("couldn't parse battery status " + ET.tostring(iq)) _LOGGER.warning("couldn't parse battery status " + ET.tostring(iq))
else: else:
self.batteryEvents.notify(self.battery_status) self.batteryEvents.notify(self.battery_status)
logging.debug("*** battery_status = {:.0%}".format(self.battery_status)) _LOGGER.debug("*** battery_status = {:.0%}".format(self.battery_status))
def _handle_charge_state(self, event): def _handle_charge_state(self, event):
status = event['type'] status = event['type']
try: try:
status = CHARGE_MODE_FROM_ECOVACS[status] status = CHARGE_MODE_FROM_ECOVACS[status]
except KeyError: except KeyError:
logging.warning("Unknown charging status '" + status + "'") _LOGGER.warning("Unknown charging status '" + status + "'")
self.charge_status = status self.charge_status = status
if status != 'idle' or self.vacuum_status == 'charging': if status != 'idle' or self.vacuum_status == 'charging':
@@ -321,18 +349,26 @@ class VacBot():
# of what the vacuum is currently up to. # of what the vacuum is currently up to.
self.vacuum_status = status self.vacuum_status = status
self.statusEvents.notify(self.vacuum_status) self.statusEvents.notify(self.vacuum_status)
logging.debug("*** charge_status = " + self.charge_status) _LOGGER.debug("*** charge_status = " + self.charge_status)
def _vacuum_address(self): def _vacuum_address(self):
return self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom' return self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom'
@property
def is_charging(self) -> bool:
return self.vacuum_status in CHARGING_STATES
@property
def is_cleaning(self) -> bool:
return self.vacuum_status in CLEANING_STATES
def send_ping(self): def send_ping(self):
try: try:
self.xmpp.send_ping(self._vacuum_address()) self.xmpp.send_ping(self._vacuum_address())
except XMPPError as err: except XMPPError as err:
logging.warning("Ping did not reach VacBot. Will retry.") _LOGGER.warning("Ping did not reach VacBot. Will retry.")
logging.debug("*** Error type: " + err.etype) _LOGGER.debug("*** Error type: " + err.etype)
logging.debug("*** Error condition: " + err.condition) _LOGGER.debug("*** Error condition: " + err.condition)
self._failed_pings += 1 self._failed_pings += 1
if self._failed_pings >= 4: if self._failed_pings >= 4:
self.vacuum_status = 'offline' self.vacuum_status = 'offline'
@@ -355,9 +391,9 @@ class VacBot():
self.run(GetLifeSpan('side_brush')) self.run(GetLifeSpan('side_brush'))
self.run(GetLifeSpan('filter')) self.run(GetLifeSpan('filter'))
except XMPPError as err: except XMPPError as err:
logging.warning("Component refresh requests failed to reach VacBot. Will try again later.") _LOGGER.warning("Component refresh requests failed to reach VacBot. Will try again later.")
logging.debug("*** Error type: " + err.etype) _LOGGER.debug("*** Error type: " + err.etype)
logging.debug("*** Error condition: " + err.condition) _LOGGER.debug("*** Error condition: " + err.condition)
def request_all_statuses(self): def request_all_statuses(self):
try: try:
@@ -365,9 +401,9 @@ class VacBot():
self.run(GetChargeState()) self.run(GetChargeState())
self.run(GetBatteryState()) self.run(GetBatteryState())
except XMPPError as err: except XMPPError as err:
logging.warning("Initial status requests failed to reach VacBot. Will try again on next ping.") _LOGGER.warning("Initial status requests failed to reach VacBot. Will try again on next ping.")
logging.debug("*** Error type: " + err.etype) _LOGGER.debug("*** Error type: " + err.etype)
logging.debug("*** Error condition: " + err.condition) _LOGGER.debug("*** Error condition: " + err.condition)
else: else:
self.refresh_components() self.refresh_components()
@@ -402,8 +438,8 @@ class EcoVacsXMPP(ClientXMPP):
self.ready_flag.wait() self.ready_flag.wait()
def session_start(self, event): def session_start(self, event):
logging.debug("----------------- starting session ----------------") _LOGGER.debug("----------------- starting session ----------------")
logging.debug("event = {}".format(event)) _LOGGER.debug("event = {}".format(event))
self.register_handler(Callback("general", self.register_handler(Callback("general",
MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}'), MatchXPath('{jabber:client}iq/{com:ctl}query/{com:ctl}'),
self._handle_ctl)) self._handle_ctl))
@@ -440,7 +476,7 @@ class EcoVacsXMPP(ClientXMPP):
def send_command(self, xml, recipient): def send_command(self, xml, recipient):
c = self._wrap_command(xml, recipient) c = self._wrap_command(xml, recipient)
logging.debug('Sending command {0}'.format(c)) _LOGGER.debug('Sending command {0}'.format(c))
c.send() c.send()
def _wrap_command(self, ctl, recipient): def _wrap_command(self, ctl, recipient):
@@ -457,7 +493,7 @@ class EcoVacsXMPP(ClientXMPP):
def send_ping(self, to): def send_ping(self, to):
q = self.make_iq_get(ito=to, ifrom=self._my_address()) q = self.make_iq_get(ito=to, ifrom=self._my_address())
q.xml.append(ET.Element('ping', {'xmlns': 'urn:xmpp:ping'})) q.xml.append(ET.Element('ping', {'xmlns': 'urn:xmpp:ping'}))
logging.debug("*** sending ping ***") _LOGGER.debug("*** sending ping ***")
q.send() q.send()
def connect_and_wait_until_ready(self): def connect_and_wait_until_ready(self):
+8 -6
View File
@@ -10,6 +10,8 @@ from pycountry_convert import country_alpha2_to_continent_code
from sucks import * from sucks import *
_LOGGER = logging.getLogger(__name__)
class FrequencyParamType(click.ParamType): class FrequencyParamType(click.ParamType):
name = 'frequency' name = 'frequency'
@@ -66,11 +68,11 @@ class StatusWait(BotWait):
def wait(self, bot): def wait(self, bot):
if not hasattr(bot, self.wait_on): if not hasattr(bot, self.wait_on):
raise ValueError("object " + bot + " does not have method " + self.wait_on) raise ValueError("object " + bot + " does not have method " + self.wait_on)
logging.debug("waiting on " + self.wait_on + " for value " + self.wait_for) _LOGGER.debug("waiting on " + self.wait_on + " for value " + self.wait_for)
while getattr(bot, self.wait_on) != self.wait_for: while getattr(bot, self.wait_on) != self.wait_for:
time.sleep(0.5) time.sleep(0.5)
logging.debug("wait complete; " + self.wait_on + " is now " + self.wait_for) _LOGGER.debug("wait complete; " + self.wait_on + " is now " + self.wait_for)
class CliAction: class CliAction:
@@ -122,15 +124,15 @@ def should_run(frequency):
return True return True
n = random.random() n = random.random()
result = n <= frequency result = n <= frequency
logging.debug("tossing coin: {:0.3f} <= {:0.3f}: {}".format(n, frequency, result)) _LOGGER.debug("tossing coin: {:0.3f} <= {:0.3f}: {}".format(n, frequency, result))
return result return result
@click.group(chain=True) @click.group(chain=True)
@click.option('--debug/--no-debug', default=False) @click.option('--debug/--no-debug', default=False)
def cli(debug): def cli(debug):
level = logging.DEBUG if debug else logging.ERROR logging.basicConfig(format='%(name)-10s %(levelname)-8s %(message)s')
logging.basicConfig(level=level, format='%(levelname)-8s %(message)s') _LOGGER.parent.setLevel(logging.DEBUG if debug else logging.ERROR)
@cli.command(help='logs in with specified email; run this first') @cli.command(help='logs in with specified email; run this first')
@@ -202,7 +204,7 @@ def run(actions, debug):
exit(1) exit(1)
if debug: if debug:
logging.debug("will run {}".format(actions)) _LOGGER.debug("will run {}".format(actions))
if actions: if actions:
config = read_config() config = read_config()
+34
View File
@@ -95,6 +95,40 @@ def test_lifespan_reports():
v._handle_ctl({'event': 'life_span', 'type': 'a_weird_component', 'total': '100', 'val': '87'}) v._handle_ctl({'event': 'life_span', 'type': 'a_weird_component', 'total': '100', 'val': '87'})
assert_equals({'side_brush': 0, 'main_brush': 0.005, 'a_weird_component': 0.87}, v.components) assert_equals({'side_brush': 0, 'main_brush': 0.005, 'a_weird_component': 0.87}, v.components)
def test_is_cleaning():
v = a_vacbot()
assert_false(v.is_cleaning)
v._handle_ctl({'event': 'clean_report', 'type': 'auto', 'speed': 'strong'})
assert_true(v.is_cleaning)
v._handle_ctl({'event': 'clean_report', 'type': 'stop'})
assert_false(v.is_cleaning)
v._handle_ctl({'event': 'clean_report', 'type': 'edge', 'speed': 'normal'})
assert_true(v.is_cleaning)
v._handle_ctl({'event': 'charge_state', 'type': 'going'})
assert_false(v.is_cleaning)
def test_is_charging():
v = a_vacbot()
assert_false(v.is_charging)
v._handle_ctl({'event': 'clean_report', 'type': 'auto', 'speed': 'strong'})
assert_false(v.is_charging)
v._handle_ctl({'event': 'charge_state', 'type': 'going'})
assert_false(v.is_charging)
v._handle_ctl({'event': 'charge_state', 'type': 'slot_charging'})
assert_true(v.is_charging)
v._handle_ctl({'event': 'clean_report', 'type': 'edge', 'speed': 'normal'})
assert_false(v.is_charging)
def test_send_ping_no_monitor(): def test_send_ping_no_monitor():
v = a_vacbot() v = a_vacbot()