device: implement UNIFIED_BATTERY feature

device: implement UNIFIED_BATTERY feature
This commit is contained in:
Peter F. Patel-Schneider 2020-10-30 10:55:55 -04:00
parent 1162ccb897
commit 733bf913e6
5 changed files with 49 additions and 20 deletions

View File

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

View File

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

View File

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

View File

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

View File

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