From 733bf913e6f15a8328e2542e104800955667ac36 Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Fri, 30 Oct 2020 10:55:55 -0400 Subject: [PATCH] device: implement UNIFIED_BATTERY feature device: implement UNIFIED_BATTERY feature --- lib/logitech_receiver/common.py | 2 ++ lib/logitech_receiver/hidpp10.py | 21 +++++++++-------- lib/logitech_receiver/hidpp20.py | 31 +++++++++++++++++++++----- lib/logitech_receiver/notifications.py | 8 +++++++ lib/logitech_receiver/status.py | 7 +++--- 5 files changed, 49 insertions(+), 20 deletions(-) diff --git a/lib/logitech_receiver/common.py b/lib/logitech_receiver/common.py index 91c8f66d..33257eb1 100644 --- a/lib/logitech_receiver/common.py +++ b/lib/logitech_receiver/common.py @@ -270,4 +270,6 @@ class KwException(Exception): """Firmware information.""" FirmwareInfo = namedtuple('FirmwareInfo', ['kind', 'name', 'version', 'extras']) +BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90) + del namedtuple diff --git a/lib/logitech_receiver/hidpp10.py b/lib/logitech_receiver/hidpp10.py index cf28fa33..e0e28493 100644 --- a/lib/logitech_receiver/hidpp10.py +++ b/lib/logitech_receiver/hidpp10.py @@ -21,6 +21,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera from logging import getLogger # , DEBUG as _DEBUG +from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import FirmwareInfo as _FirmwareInfo from .common import NamedInts as _NamedInts from .common import bytes2int as _bytes2int @@ -102,8 +103,6 @@ ERROR = _NamedInts( ) PAIRING_ERRORS = _NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06) - -BATTERY_APPOX = _NamedInts(empty=0, critical=5, low=20, good=50, full=90) """Known registers. Devices usually have a (small) sub-set of these. Some registers are only applicable to certain device kinds (e.g. smooth_scroll only applies to mice.""" @@ -211,12 +210,12 @@ def parse_battery_status(register, reply): if register == REGISTERS.battery_status: status_byte = ord(reply[:1]) charge = ( - BATTERY_APPOX.full if status_byte == 7 # full - else BATTERY_APPOX.good if status_byte == 5 # good - else BATTERY_APPOX.low if status_byte == 3 # low - else BATTERY_APPOX.critical if status_byte == 1 # critical + _BATTERY_APPROX.full if status_byte == 7 # full + else _BATTERY_APPROX.good if status_byte == 5 # good + else _BATTERY_APPROX.low if status_byte == 3 # low + else _BATTERY_APPROX.critical if status_byte == 1 # critical # pure 'charging' notifications may come without a status - else BATTERY_APPOX.empty + else _BATTERY_APPROX.empty ) charging_byte = ord(reply[1:2]) @@ -284,17 +283,17 @@ def set_3leds(device, battery_level=None, charging=None, warning=None): return if battery_level is not None: - if battery_level < BATTERY_APPOX.critical: + if battery_level < _BATTERY_APPROX.critical: # 1 orange, and force blink v1, v2 = 0x22, 0x00 warning = True - elif battery_level < BATTERY_APPOX.low: + elif battery_level < _BATTERY_APPROX.low: # 1 orange v1, v2 = 0x22, 0x00 - elif battery_level < BATTERY_APPOX.good: + elif battery_level < _BATTERY_APPROX.good: # 1 green v1, v2 = 0x20, 0x00 - elif battery_level < BATTERY_APPOX.full: + elif battery_level < _BATTERY_APPROX.full: # 2 greens v1, v2 = 0x20, 0x02 else: diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index e96f80db..dd25d976 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -28,6 +28,7 @@ from logging import getLogger from typing import List from . import special_keys +from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import FirmwareInfo as _FirmwareInfo from .common import KwException as _KwException from .common import NamedInt as _NamedInt @@ -72,6 +73,7 @@ FEATURE = _NamedInts( DFU=0x00D0, BATTERY_STATUS=0x1000, BATTERY_VOLTAGE=0x1001, + UNIFIED_BATTERY=0x1004, CHARGING_CONTROL=0x1010, LED_CONTROL=0x1300, GENERIC_TEST=0x1800, @@ -1131,14 +1133,31 @@ def get_battery(device): """Reads a device's battery level.""" battery = feature_request(device, FEATURE.BATTERY_STATUS) if battery: - discharge, dischargeNext, status = _unpack('!BBB', battery[:3]) + discharge, next, status = _unpack('!BBB', battery[:3]) discharge = None if discharge == 0 else discharge + status = BATTERY_STATUS[status] if _log.isEnabledFor(_DEBUG): - _log.debug( - 'device %d battery %s%% charged, next level %s%% charge, status %s = %s', device.number, discharge, - dischargeNext, status, BATTERY_STATUS[status] - ) - return discharge, BATTERY_STATUS[status], dischargeNext + _log.debug('device %d battery %s%% charged, next %s%%, status %s', device.number, discharge, next, status) + return discharge, status, next + else: + battery = feature_request(device, FEATURE.UNIFIED_BATTERY, 0x10) + if battery: + return decipher_unified_battery(battery) + + +def decipher_unified_battery(report): + discharge, level, status, _ignore = _unpack('!BBBB', report[:4]) + status = BATTERY_STATUS[status] + if _log.isEnabledFor(_DEBUG): + _log.debug('battery %s%% charged, level %s, charging %s', discharge, status) + level = ( + _BATTERY_APPROX.full if level == 8 # full + else _BATTERY_APPROX.good if level == 4 # good + else _BATTERY_APPROX.low if level == 2 # low + else _BATTERY_APPROX.critical if level == 1 # critical + else _BATTERY_APPROX.empty + ) + return discharge if discharge else level, status, None def get_voltage(device): diff --git a/lib/logitech_receiver/notifications.py b/lib/logitech_receiver/notifications.py index 1d85ce08..a0401840 100644 --- a/lib/logitech_receiver/notifications.py +++ b/lib/logitech_receiver/notifications.py @@ -275,6 +275,14 @@ def _process_feature_notification(device, status, n, feature): _log.warn('%s: unknown VOLTAGE %s', device, n) return True + if feature == _F.UNIFIED_BATTERY: + if n.address == 0x00: + battery_level, battery_status, battery_voltage = _hidpp20.decipher_unified_battery(n.data) + status.set_battery_info(battery_level, battery_status, None, battery_voltage) + else: + _log.warn('%s: unknown UNIFIED BATTERY %s', device, n) + return True + # TODO: what are REPROG_CONTROLS_V{2,3}? if feature == _F.REPROG_CONTROLS: if n.address == 0x00: diff --git a/lib/logitech_receiver/status.py b/lib/logitech_receiver/status.py index 651c3da4..5fbd81c1 100644 --- a/lib/logitech_receiver/status.py +++ b/lib/logitech_receiver/status.py @@ -25,6 +25,7 @@ from time import time as _timestamp from . import hidpp10 as _hidpp10 from . import hidpp20 as _hidpp20 +from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import NamedInt as _NamedInt from .common import NamedInts as _NamedInts from .i18n import _, ngettext @@ -194,11 +195,11 @@ class DeviceStatus(dict): # charging state info, so do our best to infer a level (even if it is just the last level) # It is not always possible to do this well if status == _hidpp20.BATTERY_STATUS.full: - level = _hidpp10.BATTERY_APPOX.full + level = _BATTERY_APPROX.full elif status in (_hidpp20.BATTERY_STATUS.almost_full, _hidpp20.BATTERY_STATUS.recharging): - level = _hidpp10.BATTERY_APPOX.good + level = _BATTERY_APPROX.good elif status == _hidpp20.BATTERY_STATUS.slow_recharge: - level = _hidpp10.BATTERY_APPOX.low + level = _BATTERY_APPROX.low else: level = self.get(KEYS.BATTERY_LEVEL) else: