Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up battery, alert and hidpp related code #2428

Merged
merged 5 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/hidapi/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import dataclasses


@dataclasses.dataclass
class DeviceInfo:
path: str
bus_id: str
vendor_id: str
product_id: str
interface: str
driver: str
manufacturer: str
product: str
serial: str
release: str
isDevice: bool
hidpp_short: str
hidpp_long: str
24 changes: 2 additions & 22 deletions lib/hidapi/hidapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,18 @@
import logging
import platform

from collections import namedtuple
from threading import Thread
from time import sleep

import gi

from hidapi.common import DeviceInfo

gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402

logger = logging.getLogger(__name__)

native_implementation = "hidapi"

# Device info as expected by Solaar
DeviceInfo = namedtuple(
"DeviceInfo",
[
"path",
"bus_id",
"vendor_id",
"product_id",
"interface",
"driver",
"manufacturer",
"product",
"serial",
"release",
"isDevice",
"hidpp_short",
"hidpp_long",
],
)

# Global handle to hidapi
_hidapi = None
Expand Down
23 changes: 2 additions & 21 deletions lib/hidapi/udev.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,41 +29,22 @@


# the tuple object we'll expose when enumerating devices
from collections import namedtuple
from select import select
from time import sleep
from time import time

import gi
import pyudev

from hidapi.common import DeviceInfo

gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402

logger = logging.getLogger(__name__)

native_implementation = "udev"
fileopen = open

DeviceInfo = namedtuple(
"DeviceInfo",
[
"path",
"bus_id",
"vendor_id",
"product_id",
"interface",
"driver",
"manufacturer",
"product",
"serial",
"release",
"isDevice",
"hidpp_short",
"hidpp_long",
],
)

#
# exposed API
# docstrings mostly copied from hidapi.h
Expand Down
113 changes: 61 additions & 52 deletions lib/logitech_receiver/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
# Some common functions and types.

from binascii import hexlify as _hexlify
from collections import namedtuple
from dataclasses import dataclass
from enum import IntEnum
from typing import Optional
from typing import Union

Expand All @@ -28,15 +28,6 @@
from solaar.i18n import _


def is_string(d):
return isinstance(d, str)


#
#
#


def crc16(data: bytes):
"""
CRC-16 (CCITT) implemented with a precomputed lookup table
Expand Down Expand Up @@ -314,7 +305,7 @@ class NamedInt(int):
(case-insensitive)."""

def __new__(cls, value, name):
assert is_string(name)
assert isinstance(name, str)
obj = int.__new__(cls, value)
obj.name = str(name)
return obj
Expand All @@ -329,7 +320,7 @@ def __eq__(self, other):
return int(self) == int(other) and self.name == other.name
if isinstance(other, int):
return int(self) == int(other)
if is_string(other):
if isinstance(other, str):
return self.name.lower() == other.lower()
# this should catch comparisons with bytes in Py3
if other is not None:
Expand Down Expand Up @@ -430,7 +421,7 @@ def __getitem__(self, index):
self._sort_values()
return value

elif is_string(index):
elif isinstance(index, str):
if index in self.__dict__:
return self.__dict__[index]
return next((x for x in self._values if str(x) == index), None)
Expand Down Expand Up @@ -469,7 +460,7 @@ def __setitem__(self, index, name):
if isinstance(name, NamedInt):
assert int(index) == int(name), repr(index) + " " + repr(name)
value = name
elif is_string(name):
elif isinstance(name, str):
value = NamedInt(index, name)
else:
raise TypeError("name must be a string")
Expand All @@ -490,7 +481,7 @@ def __contains__(self, value):
return self[value] == value
elif isinstance(value, int):
return value in self._indexed
elif is_string(value):
elif isinstance(value, str):
return value in self.__dict__ or value in self._values

def __iter__(self):
Expand Down Expand Up @@ -550,63 +541,81 @@ def __getattr__(self, k):
return self.args[0].get(k) # was self.args[0][k]


"""Firmware information."""
FirmwareInfo = namedtuple("FirmwareInfo", ["kind", "name", "version", "extras"])
@dataclass
class FirmwareInfo:
kind: str
name: str
version: str
extras: str


class BatteryStatus(IntEnum):
DISCHARGING = 0x00
RECHARGING = 0x01
ALMOST_FULL = 0x02
FULL = 0x03
SLOW_RECHARGE = 0x04
INVALID_BATTERY = 0x05
THERMAL_ERROR = 0x06


class BatteryLevelApproximation(IntEnum):
EMPTY = 0
CRITICAL = 5
LOW = 20
GOOD = 50
FULL = 90


@dataclass
class Battery:
"""Information about the current state of a battery"""

level: Optional[Union[NamedInt, int]]
ATTENTION_LEVEL = 5

level: Optional[Union[BatteryLevelApproximation, int]]
next_level: Optional[Union[NamedInt, int]]
status: Optional[NamedInt]
status: Optional[BatteryStatus]
voltage: Optional[int]
light_level: Optional[int] = None # light level for devices with solaar recharging

def __post_init__(self):
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 (
if self.status == BatteryStatus.FULL:
self.level = BatteryLevelApproximation.FULL
elif self.status in (BatteryStatus.ALMOST_FULL, BatteryStatus.RECHARGING):
self.level = BatteryLevelApproximation.GOOD
elif self.status == BatteryStatus.SLOW_RECHARGE:
self.level = BatteryLevelApproximation.LOW

def ok(self) -> bool:
return self.status not in (BatteryStatus.INVALID_BATTERY, BatteryStatus.THERMAL_ERROR) and (
self.level is None or self.level > Battery.ATTENTION_LEVEL
)

def charging(self):
def charging(self) -> bool:
return self.status in (
Battery.STATUS.recharging,
Battery.STATUS.almost_full,
Battery.STATUS.full,
Battery.STATUS.slow_recharge,
BatteryStatus.RECHARGING,
BatteryStatus.ALMOST_FULL,
BatteryStatus.FULL,
BatteryStatus.SLOW_RECHARGE,
)

def to_str(self):
if isinstance(self.level, NamedInt):
return _("Battery: %(level)s (%(status)s)") % {"level": _(self.level), "status": _(self.status)}
def to_str(self) -> str:
if isinstance(self.level, BatteryLevelApproximation):
level = self.level.name.lower()
status = self.status.name.lower().replace("_", " ")
return _("Battery: %(level)s (%(status)s)") % {"level": _(level), "status": _(status)}
elif isinstance(self.level, int):
return _("Battery: %(percent)d%% (%(status)s)") % {"percent": self.level, "status": _(self.status)}
status = self.status.name.lower().replace("_", " ")
return _("Battery: %(percent)d%% (%(status)s)") % {"percent": self.level, "status": _(status)}
else:
return ""


ALERT = NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF)
class Alert(IntEnum):
NONE = 0x00
NOTIFICATION = 0x01
SHOW_WINDOW = 0x02
ATTENTION = 0x04
ALL = 0xFF
15 changes: 8 additions & 7 deletions lib/logitech_receiver/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import threading as _threading
import time

from typing import Callable
from typing import Optional

import hidapi as _hid
Expand All @@ -33,7 +34,7 @@
from . import hidpp20
from . import hidpp20_constants
from . import settings
from .common import ALERT
from .common import Alert
from .common import Battery
from .settings_templates import check_feature_settings as _check_feature_settings

Expand Down Expand Up @@ -66,8 +67,8 @@ def create_device(device_info, setting_callback=None):

class Device:
instances = []
read_register = hidpp10.read_register
write_register = hidpp10.write_register
read_register: Callable = hidpp10.read_register
write_register: Callable = hidpp10.write_register

def __init__(self, receiver, number, online, pairing_info=None, handle=None, device_info=None, setting_callback=None):
assert receiver or device_info
Expand Down Expand Up @@ -374,11 +375,11 @@ def set_battery_info(self, info):
if old_info is None:
old_info = Battery(None, None, None, None)

alert, reason = ALERT.NONE, None
alert, reason = Alert.NONE, None
if not info.ok():
logger.warning("%s: battery %d%%, ALERT %s", self, info.level, info.status)
if old_info.status != info.status:
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
alert = Alert.NOTIFICATION | Alert.ATTENTION
reason = info.to_str()

if changed or reason:
Expand All @@ -392,7 +393,7 @@ def read_battery(self):
battery = self.battery()
self.set_battery_info(battery if battery is not None else Battery(None, None, None, None))

def changed(self, active=None, alert=ALERT.NONE, reason=None, push=False):
def changed(self, active=None, alert=Alert.NONE, reason=None, push=False):
"""The status of the device had changed, so invoke the status callback.
Also push notifications and settings to the device when necessary."""
if active is not None:
Expand All @@ -417,7 +418,7 @@ def changed(self, active=None, alert=ALERT.NONE, reason=None, push=False):
self.set_configuration(0x11) # signal end of configuration
self.read_battery() # battery information may have changed so try to read it now
elif was_active and self.receiver: # need to set configuration pending flag in receiver
hidpp10.Hidpp10().set_configuration_pending_flags(self.receiver, 0xFF)
hidpp10.set_configuration_pending_flags(self.receiver, 0xFF)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("device %d changed: active=%s %s", self.number, self._active, self.battery_info)
if self.status_callback is not None:
Expand Down
Loading
Loading