device: change status battery fields to Battery objects

This commit is contained in:
Peter F. Patel-Schneider 2024-03-06 17:12:22 -05:00
parent 3916c189be
commit 64d8cad81a
6 changed files with 128 additions and 183 deletions

View File

@ -1,4 +1,5 @@
## Copyright (C) 2012-2013 Daniel Pavel ## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## ##
## This program is free software; you can redistribute it and/or modify ## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by ## it under the terms of the GNU General Public License as published by
@ -18,9 +19,13 @@
from binascii import hexlify as _hexlify from binascii import hexlify as _hexlify
from collections import namedtuple from collections import namedtuple
from dataclasses import dataclass
from typing import Optional
import yaml as _yaml import yaml as _yaml
from .i18n import _
def is_string(d): def is_string(d):
return isinstance(d, str) return isinstance(d, str)
@ -546,18 +551,57 @@ class KwException(Exception):
"""Firmware information.""" """Firmware information."""
FirmwareInfo = namedtuple("FirmwareInfo", ["kind", "name", "version", "extras"]) FirmwareInfo = namedtuple("FirmwareInfo", ["kind", "name", "version", "extras"])
BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90)
BATTERY_STATUS = NamedInts( @dataclass
discharging=0x00, class Battery:
recharging=0x01, """Information about the current state of a battery"""
almost_full=0x02,
full=0x03,
slow_recharge=0x04,
invalid_battery=0x05,
thermal_error=0x06,
)
level: Optional[NamedInt | int]
next_level: Optional[NamedInt | int]
status: Optional[NamedInt]
voltage: Optional[int]
light_level: Optional[int] = None # light level for devices with solaar recharging
def BATTERY_OK(status): def __post_init__(self):
return status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error) if self.level is None: # infer level from status if needed and possible
if self.status == Battery.STATUS.full:
self.level = Battery.APPROX.full
elif self.status in (Battery.STATUS.almost_full, Battery.STATUS.recharging):
self.level = Battery.APPROX.good
elif self.status == Battery.STATUS.slow_recharge:
self.level = Battery.APPROX.low
STATUS = NamedInts(
discharging=0x00,
recharging=0x01,
almost_full=0x02,
full=0x03,
slow_recharge=0x04,
invalid_battery=0x05,
thermal_error=0x06,
)
APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90)
ATTENTION_LEVEL = 5
def ok(self):
return self.status not in (Battery.STATUS.invalid_battery, Battery.STATUS.thermal_error) and (
self.level is None or self.level > Battery.ATTENTION_LEVEL
)
def charging(self):
return self.status in (
Battery.STATUS.recharging,
Battery.STATUS.almost_full,
Battery.STATUS.full,
Battery.STATUS.slow_recharge,
)
def to_str(self):
if isinstance(self.level, NamedInt):
return _("Battery: %(level)s (%(status)s)") % {"level": _(self.level), "status": _(self.status)}
elif isinstance(self.level, int):
return _("Battery: %(percent)d%% (%(status)s)") % {"percent": self.level, "status": _(self.status)}
else:
return ""

View File

@ -342,10 +342,10 @@ class Device:
if battery_feature != 0: if battery_feature != 0:
result = _hidpp20.get_battery(self, battery_feature) result = _hidpp20.get_battery(self, battery_feature)
try: try:
feature, level, next, status, voltage = result feature, battery = result
if self.persister and battery_feature is None: if self.persister and battery_feature is None:
self.persister["_battery"] = feature self.persister["_battery"] = feature
return level, next, status, voltage return battery
except Exception: except Exception:
if self.persister and battery_feature is None: if self.persister and battery_feature is None:
self.persister["_battery"] = result self.persister["_battery"] = result

View File

@ -16,8 +16,7 @@
import logging import logging
from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import Battery as _Battery
from .common import BATTERY_STATUS as _BATTERY_STATUS
from .common import FirmwareInfo as _FirmwareInfo from .common import FirmwareInfo as _FirmwareInfo
from .common import bytes2int as _bytes2int from .common import bytes2int as _bytes2int
from .common import int2bytes as _int2bytes from .common import int2bytes as _int2bytes
@ -122,17 +121,17 @@ class Hidpp10:
return return
if battery_level is not None: if battery_level is not None:
if battery_level < _BATTERY_APPROX.critical: if battery_level < _Battery.APPROX.critical:
# 1 orange, and force blink # 1 orange, and force blink
v1, v2 = 0x22, 0x00 v1, v2 = 0x22, 0x00
warning = True warning = True
elif battery_level < _BATTERY_APPROX.low: elif battery_level < _Battery.APPROX.low:
# 1 orange # 1 orange
v1, v2 = 0x22, 0x00 v1, v2 = 0x22, 0x00
elif battery_level < _BATTERY_APPROX.good: elif battery_level < _Battery.APPROX.good:
# 1 green # 1 green
v1, v2 = 0x20, 0x00 v1, v2 = 0x20, 0x00
elif battery_level < _BATTERY_APPROX.full: elif battery_level < _Battery.APPROX.full:
# 2 greens # 2 greens
v1, v2 = 0x20, 0x02 v1, v2 = 0x20, 0x02
else: else:
@ -196,38 +195,38 @@ def parse_battery_status(register, reply):
charge = ord(reply[:1]) charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0 status_byte = ord(reply[2:3]) & 0xF0
status_text = ( status_text = (
_BATTERY_STATUS.discharging _Battery.STATUS.discharging
if status_byte == 0x30 if status_byte == 0x30
else _BATTERY_STATUS.recharging else _Battery.STATUS.recharging
if status_byte == 0x50 if status_byte == 0x50
else _BATTERY_STATUS.full else _Battery.STATUS.full
if status_byte == 0x90 if status_byte == 0x90
else None else None
) )
return charge, None, status_text, None return _Battery(charge, None, status_text, None)
if register == REGISTERS.battery_status: if register == REGISTERS.battery_status:
status_byte = ord(reply[:1]) status_byte = ord(reply[:1])
charge = ( charge = (
_BATTERY_APPROX.full _Battery.APPROX.full
if status_byte == 7 # full if status_byte == 7 # full
else _BATTERY_APPROX.good else _Battery.APPROX.good
if status_byte == 5 # good if status_byte == 5 # good
else _BATTERY_APPROX.low else _Battery.APPROX.low
if status_byte == 3 # low if status_byte == 3 # low
else _BATTERY_APPROX.critical else _Battery.APPROX.critical
if status_byte == 1 # critical if status_byte == 1 # critical
# pure 'charging' notifications may come without a status # pure 'charging' notifications may come without a status
else _BATTERY_APPROX.empty else _Battery.APPROX.empty
) )
charging_byte = ord(reply[1:2]) charging_byte = ord(reply[1:2])
if charging_byte == 0x00: if charging_byte == 0x00:
status_text = _BATTERY_STATUS.discharging status_text = _Battery.STATUS.discharging
elif charging_byte & 0x21 == 0x21: elif charging_byte & 0x21 == 0x21:
status_text = _BATTERY_STATUS.recharging status_text = _Battery.STATUS.recharging
elif charging_byte & 0x22 == 0x22: elif charging_byte & 0x22 == 0x22:
status_text = _BATTERY_STATUS.full status_text = _Battery.STATUS.full
else: else:
logger.warning("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte) logger.warning("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
status_text = None status_text = None
@ -237,4 +236,4 @@ def parse_battery_status(register, reply):
charge = None charge = None
# Return None for next charge level and voltage as these are not in HID++ 1.0 spec # Return None for next charge level and voltage as these are not in HID++ 1.0 spec
return charge, None, status_text, None return _Battery(charge, None, status_text, None)

View File

@ -27,8 +27,7 @@ import yaml as _yaml
from . import exceptions, special_keys from . import exceptions, special_keys
from . import hidpp10_constants as _hidpp10_constants from . import hidpp10_constants as _hidpp10_constants
from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import Battery
from .common import BATTERY_STATUS as _BATTERY_STATUS
from .common import FirmwareInfo as _FirmwareInfo from .common import FirmwareInfo as _FirmwareInfo
from .common import NamedInt as _NamedInt from .common import NamedInt as _NamedInt
from .common import NamedInts as _NamedInts from .common import NamedInts as _NamedInts
@ -1730,31 +1729,31 @@ battery_functions = {
def decipher_battery_status(report): def decipher_battery_status(report):
discharge, next, status = _unpack("!BBB", report[:3]) discharge, next, status = _unpack("!BBB", report[:3])
discharge = None if discharge == 0 else discharge discharge = None if discharge == 0 else discharge
status = _BATTERY_STATUS[status] status = Battery.STATUS[status]
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("battery status %s%% charged, next %s%%, status %s", discharge, next, status) logger.debug("battery status %s%% charged, next %s%%, status %s", discharge, next, status)
return FEATURE.BATTERY_STATUS, discharge, next, status, None return FEATURE.BATTERY_STATUS, Battery(discharge, next, status, None)
def decipher_battery_voltage(report): def decipher_battery_voltage(report):
voltage, flags = _unpack(">HB", report[:3]) voltage, flags = _unpack(">HB", report[:3])
status = _BATTERY_STATUS.discharging status = Battery.STATUS.discharging
charge_sts = ERROR.unknown charge_sts = ERROR.unknown
charge_lvl = CHARGE_LEVEL.average charge_lvl = CHARGE_LEVEL.average
charge_type = CHARGE_TYPE.standard charge_type = CHARGE_TYPE.standard
if flags & (1 << 7): if flags & (1 << 7):
status = _BATTERY_STATUS.recharging status = Battery.STATUS.recharging
charge_sts = CHARGE_STATUS[flags & 0x03] charge_sts = CHARGE_STATUS[flags & 0x03]
if charge_sts is None: if charge_sts is None:
charge_sts = ERROR.unknown charge_sts = ERROR.unknown
elif charge_sts == CHARGE_STATUS.full: elif charge_sts == CHARGE_STATUS.full:
charge_lvl = CHARGE_LEVEL.full charge_lvl = CHARGE_LEVEL.full
status = _BATTERY_STATUS.full status = Battery.STATUS.full
if flags & (1 << 3): if flags & (1 << 3):
charge_type = CHARGE_TYPE.fast charge_type = CHARGE_TYPE.fast
elif flags & (1 << 4): elif flags & (1 << 4):
charge_type = CHARGE_TYPE.slow charge_type = CHARGE_TYPE.slow
status = _BATTERY_STATUS.slow_recharge status = Battery.STATUS.slow_recharge
elif flags & (1 << 5): elif flags & (1 << 5):
charge_lvl = CHARGE_LEVEL.critical charge_lvl = CHARGE_LEVEL.critical
for level in battery_voltage_remaining: for level in battery_voltage_remaining:
@ -1771,26 +1770,26 @@ def decipher_battery_voltage(report):
charge_lvl, charge_lvl,
charge_type, charge_type,
) )
return FEATURE.BATTERY_VOLTAGE, charge_lvl, None, status, voltage return FEATURE.BATTERY_VOLTAGE, Battery(charge_lvl, None, status, voltage)
def decipher_battery_unified(report): def decipher_battery_unified(report):
discharge, level, status, _ignore = _unpack("!BBBB", report[:4]) discharge, level, status, _ignore = _unpack("!BBBB", report[:4])
status = _BATTERY_STATUS[status] status = Battery.STATUS[status]
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("battery unified %s%% charged, level %s, charging %s", discharge, level, status) logger.debug("battery unified %s%% charged, level %s, charging %s", discharge, level, status)
level = ( level = (
_BATTERY_APPROX.full Battery.APPROX.full
if level == 8 # full if level == 8 # full
else _BATTERY_APPROX.good else Battery.APPROX.good
if level == 4 # good if level == 4 # good
else _BATTERY_APPROX.low else Battery.APPROX.low
if level == 2 # low if level == 2 # low
else _BATTERY_APPROX.critical else Battery.APPROX.critical
if level == 1 # critical if level == 1 # critical
else _BATTERY_APPROX.empty else Battery.APPROX.empty
) )
return FEATURE.UNIFIED_BATTERY, discharge if discharge else level, None, status, None return FEATURE.UNIFIED_BATTERY, Battery(discharge if discharge else level, None, status, None)
def decipher_adc_measurement(report): def decipher_adc_measurement(report):
@ -1801,5 +1800,5 @@ def decipher_adc_measurement(report):
charge_level = level[1] charge_level = level[1]
break break
if flags & 0x01: if flags & 0x01:
status = _BATTERY_STATUS.recharging if flags & 0x02 else _BATTERY_STATUS.discharging status = Battery.STATUS.recharging if flags & 0x02 else Battery.STATUS.discharging
return FEATURE.ADC_MEASUREMENT, charge_level, None, status, adc return FEATURE.ADC_MEASUREMENT, Battery(charge_level, None, status, adc)

View File

@ -29,7 +29,7 @@ from . import hidpp10_constants as _hidpp10_constants
from . import hidpp20_constants as _hidpp20_constants from . import hidpp20_constants as _hidpp20_constants
from . import settings_templates as _st from . import settings_templates as _st
from .base import DJ_MESSAGE_ID as _DJ_MESSAGE_ID from .base import DJ_MESSAGE_ID as _DJ_MESSAGE_ID
from .common import BATTERY_STATUS as _BATTERY_STATUS from .common import Battery as _Battery
from .common import strhex as _strhex from .common import strhex as _strhex
from .i18n import _ from .i18n import _
from .status import ALERT as _ALERT from .status import ALERT as _ALERT
@ -218,11 +218,9 @@ def _process_hidpp10_custom_notification(device, status, n):
logger.debug("%s (%s) custom notification %s", device, device.protocol, n) logger.debug("%s (%s) custom notification %s", device, device.protocol, n)
if n.sub_id in (_R.battery_status, _R.battery_charge): if n.sub_id in (_R.battery_status, _R.battery_charge):
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
assert n.data[-1:] == b"\x00" assert n.data[-1:] == b"\x00"
data = chr(n.address).encode() + n.data data = chr(n.address).encode() + n.data
charge, next_charge, status_text, voltage = hidpp10.parse_battery_status(n.sub_id, data) status.set_battery_info(hidpp10.parse_battery_status(n.sub_id, data))
status.set_battery_info(charge, next_charge, status_text, voltage)
return True return True
logger.warning("%s: unrecognized %s", device, n) logger.warning("%s: unrecognized %s", device, n)
@ -296,8 +294,7 @@ def _process_feature_notification(device, status, n, feature):
if feature == _F.BATTERY_STATUS: if feature == _F.BATTERY_STATUS:
if n.address == 0x00: if n.address == 0x00:
_ignore, discharge_level, discharge_next_level, battery_status, voltage = hidpp20.decipher_battery_status(n.data) status.set_battery_info(hidpp20.decipher_battery_status(n.data)[1])
status.set_battery_info(discharge_level, discharge_next_level, battery_status, voltage)
elif n.address == 0x10: elif n.address == 0x10:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info("%s: spurious BATTERY status %s", device, n) logger.info("%s: spurious BATTERY status %s", device, n)
@ -306,15 +303,13 @@ def _process_feature_notification(device, status, n, feature):
elif feature == _F.BATTERY_VOLTAGE: elif feature == _F.BATTERY_VOLTAGE:
if n.address == 0x00: if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = hidpp20.decipher_battery_voltage(n.data) status.set_battery_info(hidpp20.decipher_battery_voltage(n.data)[1])
status.set_battery_info(level, nextl, battery_status, voltage)
else: else:
logger.warning("%s: unknown VOLTAGE %s", device, n) logger.warning("%s: unknown VOLTAGE %s", device, n)
elif feature == _F.UNIFIED_BATTERY: elif feature == _F.UNIFIED_BATTERY:
if n.address == 0x00: if n.address == 0x00:
_ignore, level, nextl, battery_status, voltage = hidpp20.decipher_battery_unified(n.data) status.set_battery_info(hidpp20.decipher_battery_unified(n.data)[1])
status.set_battery_info(level, nextl, battery_status, voltage)
else: else:
logger.warning("%s: unknown UNIFIED BATTERY %s", device, n) logger.warning("%s: unknown UNIFIED BATTERY %s", device, n)
@ -322,8 +317,7 @@ def _process_feature_notification(device, status, n, feature):
if n.address == 0x00: if n.address == 0x00:
result = hidpp20.decipher_adc_measurement(n.data) result = hidpp20.decipher_adc_measurement(n.data)
if result: if result:
_ignore, level, nextl, battery_status, voltage = result status.set_battery_info(result[1])
status.set_battery_info(level, nextl, battery_status, voltage)
else: # this feature is used to signal device becoming inactive else: # this feature is used to signal device becoming inactive
status.changed(active=False) status.changed(active=False)
else: else:
@ -334,15 +328,13 @@ def _process_feature_notification(device, status, n, feature):
charge, lux, adc = _unpack("!BHH", n.data[:5]) charge, lux, adc = _unpack("!BHH", n.data[:5])
# guesstimate the battery voltage, emphasis on 'guess' # guesstimate the battery voltage, emphasis on 'guess'
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672) # status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
status_text = _BATTERY_STATUS.discharging status_text = _Battery.STATUS.discharging
if n.address == 0x00: if n.address == 0x00:
status[_K.LIGHT_LEVEL] = None status.set_battery_info(_Battery(charge, None, status_text, None))
status.set_battery_info(charge, None, status_text, None)
elif n.address == 0x10: elif n.address == 0x10:
status[_K.LIGHT_LEVEL] = lux
if lux > 200: if lux > 200:
status_text = _BATTERY_STATUS.recharging status_text = _Battery.STATUS.recharging
status.set_battery_info(charge, None, status_text, None) status.set_battery_info(_Battery(charge, None, status_text, None, lux))
elif n.address == 0x20: elif n.address == 0x20:
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: Light Check button pressed", device) logger.debug("%s: Light Check button pressed", device)

View File

@ -1,4 +1,5 @@
## Copyright (C) 2012-2013 Daniel Pavel ## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
## ##
## This program is free software; you can redistribute it and/or modify ## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by ## it under the terms of the GNU General Public License as published by
@ -20,11 +21,7 @@ from . import hidpp10
from . import hidpp10_constants as _hidpp10_constants from . import hidpp10_constants as _hidpp10_constants
from . import hidpp20_constants as _hidpp20_constants from . import hidpp20_constants as _hidpp20_constants
from . import settings as _settings from . import settings as _settings
from .common import BATTERY_APPROX as _BATTERY_APPROX from .common import Battery, NamedInts
from .common import BATTERY_OK as _BATTERY_OK
from .common import BATTERY_STATUS as _BATTERY_STATUS
from .common import NamedInt as _NamedInt
from .common import NamedInts as _NamedInts
from .i18n import _, ngettext from .i18n import _, ngettext
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,27 +30,9 @@ _R = _hidpp10_constants.REGISTERS
_hidpp10 = hidpp10.Hidpp10() _hidpp10 = hidpp10.Hidpp10()
# ALERT = NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF)
#
#
ALERT = _NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF) KEYS = NamedInts(LINK_ENCRYPTED=5, NOTIFICATION_FLAGS=6, ERROR=7)
KEYS = _NamedInts(
BATTERY_LEVEL=1,
BATTERY_CHARGING=2,
BATTERY_STATUS=3,
LIGHT_LEVEL=4,
LINK_ENCRYPTED=5,
NOTIFICATION_FLAGS=6,
ERROR=7,
BATTERY_NEXT_LEVEL=8,
BATTERY_VOLTAGE=9,
)
# If the battery charge is under this percentage, trigger an attention event
# (blink systray icon/notification/whatever).
_BATTERY_ATTENTION_LEVEL = 5
def attach_to(device, changed_callback): def attach_to(device, changed_callback):
@ -67,11 +46,6 @@ def attach_to(device, changed_callback):
device.status = DeviceStatus(device, changed_callback) device.status = DeviceStatus(device, changed_callback)
#
#
#
class ReceiverStatus(dict): class ReceiverStatus(dict):
"""The 'runtime' status of a receiver, mostly about the pairing process -- """The 'runtime' status of a receiver, mostly about the pairing process --
is the pairing lock open or closed, any pairing errors, etc. is the pairing lock open or closed, any pairing errors, etc.
@ -111,15 +85,10 @@ class ReceiverStatus(dict):
self._changed_callback(self._receiver, alert=alert, reason=reason) self._changed_callback(self._receiver, alert=alert, reason=reason)
#
#
#
class DeviceStatus(dict): class DeviceStatus(dict):
"""Holds the 'runtime' status of a peripheral -- things like """Holds the 'runtime' status of a peripheral
active/inactive, battery charge, lux, etc. It updates them mostly by Currently _active, battery -- dict entries are being moved to attributs
processing incoming notification events from the device itself. Updates mostly come from incoming notification events from the device itself.
""" """
def __init__(self, device, changed_callback): def __init__(self, device, changed_callback):
@ -128,19 +97,10 @@ class DeviceStatus(dict):
assert changed_callback assert changed_callback
self._changed_callback = changed_callback self._changed_callback = changed_callback
self._active = None # is the device active? self._active = None # is the device active?
self.battery = None
def to_string(self): def to_string(self):
status = "" return self.battery.to_str() if self.battery is not None else ""
battery_level = self.get(KEYS.BATTERY_LEVEL)
if battery_level is not None:
if isinstance(battery_level, _NamedInt):
status = _("Battery: %(level)s") % {"level": _(str(battery_level))}
else:
status = _("Battery: %(percent)d%%") % {"percent": battery_level}
battery_status = self.get(KEYS.BATTERY_STATUS)
if battery_status is not None:
status += " (%s)" % _(str(battery_status))
return status
def __repr__(self): def __repr__(self):
return "{" + ", ".join("'%s': %r" % (k, v) for k, v in self.items()) + "}" return "{" + ", ".join("'%s': %r" % (k, v) for k, v in self.items()) + "}"
@ -150,81 +110,38 @@ class DeviceStatus(dict):
__nonzero__ = __bool__ __nonzero__ = __bool__
def set_battery_info(self, level, nextLevel, status, voltage): def set_battery_info(self, info):
if logger.isEnabledFor(logging.DEBUG): if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: battery %s, %s", self._device, level, status) logger.debug("%s: battery %s, %s", self._device, info.level, info.status)
if info.level is None and self.battery: # use previous level if missing from new information
info.level = self.battery.level
if level is None: changed = self.battery != info
# Some notifications may come with no battery level info, just self.battery = info
# 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 == _BATTERY_STATUS.full:
level = _BATTERY_APPROX.full
elif status in (_BATTERY_STATUS.almost_full, _BATTERY_STATUS.recharging):
level = _BATTERY_APPROX.good
elif status == _BATTERY_STATUS.slow_recharge:
level = _BATTERY_APPROX.low
else:
level = self.get(KEYS.BATTERY_LEVEL)
else:
assert isinstance(level, int)
# TODO: this is also executed when pressing Fn+F7 on K800.
old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level
old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status
self[KEYS.BATTERY_NEXT_LEVEL] = nextLevel
old_voltage, self[KEYS.BATTERY_VOLTAGE] = self.get(KEYS.BATTERY_VOLTAGE), voltage
charging = status in (
_BATTERY_STATUS.recharging,
_BATTERY_STATUS.almost_full,
_BATTERY_STATUS.full,
_BATTERY_STATUS.slow_recharge,
)
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
changed = old_level != level or old_status != status or old_charging != charging or old_voltage != voltage
alert, reason = ALERT.NONE, None alert, reason = ALERT.NONE, None
if info.ok():
if _BATTERY_OK(status) and (level is None or level > _BATTERY_ATTENTION_LEVEL):
self[KEYS.ERROR] = None self[KEYS.ERROR] = None
else: else:
logger.warning("%s: battery %d%%, ALERT %s", self._device, level, status) logger.warning("%s: battery %d%%, ALERT %s", self._device, info.level, info.status)
if self.get(KEYS.ERROR) != status: if self.get(KEYS.ERROR) != info.status:
self[KEYS.ERROR] = status self[KEYS.ERROR] = info.status
# only show the notification once
alert = ALERT.NOTIFICATION | ALERT.ATTENTION alert = ALERT.NOTIFICATION | ALERT.ATTENTION
if isinstance(level, _NamedInt): reason = info.to_str()
reason = _("Battery: %(level)s (%(status)s)") % {"level": _(level), "status": _(status)}
else:
reason = _("Battery: %(percent)d%% (%(status)s)") % {"percent": level, "status": status.name}
if changed or reason or not self._active: # a battery response means device is active if changed or reason or not self._active: # a battery response means device is active
# update the leds on the device, if any # update the leds on the device, if any
_hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert)) _hidpp10.set_3leds(self._device, info.level, charging=info.charging(), warning=bool(alert))
self.changed(active=True, alert=alert, reason=reason) self.changed(active=True, alert=alert, reason=reason)
# Retrieve and regularize battery status # Retrieve and regularize battery status
def read_battery(self): def read_battery(self):
if self._active: if self._active:
assert self._device
battery = self._device.battery() battery = self._device.battery()
self.set_battery_keys(battery) self.set_battery_info(battery if battery is not None else Battery(None, None, None, None))
def set_battery_keys(self, battery):
if battery is not None:
level, nextLevel, status, voltage = battery
self.set_battery_info(level, nextLevel, status, voltage)
elif self.get(KEYS.BATTERY_STATUS, None) is not None:
self[KEYS.BATTERY_STATUS] = None
self[KEYS.BATTERY_CHARGING] = None
self[KEYS.BATTERY_VOLTAGE] = None
self.changed()
def changed(self, active=None, alert=ALERT.NONE, reason=None, push=False): def changed(self, active=None, alert=ALERT.NONE, reason=None, push=False):
assert self._changed_callback
d = self._device d = self._device
# assert d # may be invalid when processing the 'unpaired' notification
if active is not None: if active is not None:
d.online = active d.online = active
@ -236,12 +153,10 @@ class DeviceStatus(dict):
# goes idle, and we can't tell the difference right now). # goes idle, and we can't tell the difference right now).
if d.protocol < 2.0: if d.protocol < 2.0:
self[KEYS.NOTIFICATION_FLAGS] = d.enable_connection_notifications() self[KEYS.NOTIFICATION_FLAGS] = d.enable_connection_notifications()
# battery information may have changed so try to read it now # battery information may have changed so try to read it now
self.read_battery() self.read_battery()
# Push settings for new devices (was_active is None), # Push settings for new devices when devices request software reconfiguration
# when devices request software reconfiguration
# and when devices become active if they don't have wireless device status feature, # and when devices become active if they don't have wireless device status feature,
if ( if (
was_active is None was_active is None
@ -253,10 +168,6 @@ class DeviceStatus(dict):
logger.info("%s pushing device settings %s", d, d.settings) logger.info("%s pushing device settings %s", d, d.settings)
_settings.apply_all_settings(d) _settings.apply_all_settings(d)
else: if logger.isEnabledFor(logging.DEBUG):
if was_active: # don't clear status when devices go inactive logger.debug("device %d changed: active=%s %s", d.number, self._active, self.battery)
pass
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
self._changed_callback(d, alert, reason) self._changed_callback(d, alert, reason)