Source code for switchbot_api.bot_information
'''
Python-Switchbot-BLE: A Python library for interfacing with Switchbot devices over Bluetooth Low Energy (BLE)
Copyright (C) 2023 Benjamin Carlson
'''
from typing import Optional, List, Dict
import zlib
from datetime import timedelta
from .bot_types import SwitchBotDeviceType, SwitchBotMode, SwitchBotGroup
from .alarm_info import AlarmInfo, AlarmExecAction, AlarmExecType, DayOfWeek
[docs]class BotInformation:
def __init__(self, get_info_byte_array: Optional[bytearray] = None):
"""
Initialize information class with "Get Information" bytes if present
:param get_info_byte_array: The byte data from the Get Information request
:type get_info_byte_array: Optional[bytearray]
"""
# Start Device Info
self._remaining_battery_percent = 100 # 0-100
self._firmware_version: float = 6.3
self._push_button_strength = 100 # 0-100
self._sensor_adc_value = 0 # Analog Digital Converter value
self._motor_calibration_val = 0xA1 # Motor calibration value
self._time_number = 0 # Number of the timer?
self._bot_act_mode = 0 # Bot action mode, TOOD: Convert to new enum
self._hold_and_press_times = 0 # Default
self._is_encrypted = False
self._device_type = SwitchBotDeviceType.BOT
self._bot_mode = SwitchBotMode.ONE_STATE # Default
self._is_off = True
self._encryption_type = 0 # (0 = standard checksum, 1 = TBD)
self._device_groups: List[SwitchBotGroup] = []
self._current_pass_str: Optional[str] = None
self._current_pass_checksum: Optional[bytearray] = None
# End Device Info
# Start time management info
# UNIX time since epoch
self._current_timestamp = 0
self._alarm_count = 0
self._alarm_infos: Dict[int, AlarmInfo] = {}
# End time management info
if get_info_byte_array is not None:
self._init_from_byte_array(get_info_byte_array)
def _init_from_byte_array(self, get_info_byte_array: bytearray):
"""
Initialize data from "Get Information" bytes
:param get_info_byte_array: The byte data from the Get Information request
:type get_info_byte_array: bytearray
"""
if len(get_info_byte_array) != 12:
raise ValueError(
f"Invalid byte array length ({len(get_info_byte_array)})for BotInformation"
)
# For 1 byte values
int_data = [int(byte) for byte in get_info_byte_array]
self._remaining_battery_percent = int_data[0] # 0-100
self._firmware_version: float = int_data[1] * 0.1 # 44 * 0.1 = 4.4 Firmware version
self._push_button_strength = int_data[2] # 0-100
self._sensor_adc_value = int.from_bytes(
get_info_byte_array[3:5], byteorder="big"
) # Analog Digital Converter value
self._motor_calibration_val = int.from_bytes(
get_info_byte_array[5:7], byteorder="big"
) # Motor calibration value
self._time_number = int_data[7] # Number of the timer?
self._bot_act_mode = int_data[8] # Bot action mode, TOOD: Convert to new enum
self._hold_and_press_times = int_data[9] # Hold-and-press times
self.read_service_bytes(get_info_byte_array[10:12])
[docs] def read_service_bytes(self, service_bytes: bytearray) -> None:
"""
Update object from service bytes (either from advertisement or Get Information)
:param service_bytes: Service byte array
:type service_bytes: bytearray
"""
if len(service_bytes) < 2 or len(service_bytes) > 3:
raise ValueError(
f"Invalid service bytes length ({len(service_bytes)})for BotInformation"
)
# Might be little endian for broadcasting, currently assuming big endian (from BLE requests)
# Handle Encrypted/Device Type Byte
enc_dev_type_byte = service_bytes[0]
self._is_encrypted = (enc_dev_type_byte & 0x80) == 0x80 # First bit is 1 if encrypted
device_type_int = int(enc_dev_type_byte & 0x7F) # Last 7 bits are the device type
try:
self._device_type = SwitchBotDeviceType(device_type_int)
except ValueError:
print(f"Unknown device type: {device_type_int}")
pass
# End Encrypted/Device Type Byte
# Handle Status Byte
status_byte = service_bytes[1]
bot_mode_int = int(
(status_byte & 0x80) == 0x80
) # First bit is the bot mode (0 = one state, 1 = on/off state)
self._bot_mode = SwitchBotMode(bot_mode_int)
self._is_off = (
status_byte & 0x40
) == 0x40 # Second bit is the current bot state (0 = on, 1 = off)
self._encryption_type = int(
(status_byte & 0x20) == 0x20
) # Third bit is the encryption type (0 = standard checksum, 1 = TBD)
# A little bit unsure what to do with this
has_service_data_update = (
status_byte & 0x10
) == 0x10 # Fourth bit is if the service data has been updated (0 = no, 1 = yes)
self._device_groups = [
group for group in SwitchBotGroup if (status_byte & group.value) == group.value
] # Last 4 bits are the device group membership
# End Status Byte
if len(service_bytes) > 2:
update_utc_flag_bat_byte = service_bytes[2]
does_require_utc_sync = (
update_utc_flag_bat_byte & 0x80
) == 0x80 # First bit is if the device requires a UTC sync
self._remaining_battery_percent = int(
update_utc_flag_bat_byte & 0x7F
) # Last 7 bits are the remaining battery percent
# Basic Info Properties
@property
def password_str(self) -> Optional[str]:
return self._current_pass_str
@password_str.setter
def password_str(self, password_str: Optional[str]):
"""
Sets the password (or clears with None) and computes the password checksum
:param password_str: The new password string
:type password_str: Optional[str]
"""
if password_str is None:
self._is_encrypted = False
self._current_pass_str = None
self._current_pass_checksum = None
return
self._current_pass_str = password_str
self._current_pass_checksum = zlib.crc32(self._current_pass_str.encode()).to_bytes(
4, byteorder="big"
)
self._is_encrypted = True
@property
def password_checksum(self) -> Optional[bytearray]:
return self._current_pass_checksum
@property
def is_encrypted(self) -> bool:
return self._is_encrypted
@property
def remaining_battery_percent(self) -> int:
return self._remaining_battery_percent
@property
def firmware_version(self) -> float:
return self._firmware_version
@property
def push_button_strength(self) -> int:
return self._push_button_strength
@property
def sensor_adc_value(self) -> int:
return self._sensor_adc_value
@property
def motor_calibration_val(self) -> int:
return self._motor_calibration_val
@property
def time_number(self) -> int:
return self._time_number
@property
def bot_action_mode(self) -> int:
return self._bot_act_mode
@property
def hold_and_press_times(self) -> int:
return self._hold_and_press_times
@property
def device_type(self) -> SwitchBotDeviceType:
return self._device_type
@property
def bot_mode(self) -> SwitchBotMode:
return self._bot_mode
@property
def is_off(self) -> bool:
return self._is_off
@property
def encryption_type(self) -> int:
return self._encryption_type
@property
def device_groups(self) -> List[SwitchBotGroup]:
return self._device_groups
# End Basic Info Properties
# Start Time Management Properties
@property
def alarm_count(self) -> int:
return self._alarm_count
@alarm_count.setter
def alarm_count(self, count: int):
"""
Updates alarm count (and performs 0 <= n <= 4 bounds checking)
:param count: The amount of alarms
:type count: int
"""
if count < 0 or count > 4:
print(f"Invalid alarm count {count}, must be between 0 and 4!")
raise UserWarning(f"Invalid alarm count {count}, must be between 0 and 4!")
self._alarm_count = count
@property
def system_timestamp(self) -> int:
return self._current_timestamp
@system_timestamp.setter
def system_timestamp(self, timestamp: int):
"""
Updates the timestamp (and performs 0 <= t bounds checking)
:param timestamp: SwitchBot Unix timestamp
:type timestamp: int
"""
if timestamp < 0:
print(f"Invalid timestamp {timestamp}, must be greater than 0!")
raise UserWarning(f"Invalid timestamp {timestamp}, must be greater than 0!")
self._current_timestamp = timestamp
@property
def active_alarms(self) -> List[AlarmInfo]:
return [info for _, info in self._alarm_infos.items()]
[docs] def update_alarm(self, response_data: bytearray):
"""
Update alarm info from fetch alarm info request
:param response_data: response bytes
:type response_data: bytearray
"""
if len(response_data) != 11:
print(
f"Could not update alarm, invalid response data length {len(response_data)} (Must be 11)"
)
alarm_count = response_data[0]
alarm_idx = response_data[1]
exec_repeatedly = response_data[2] >> 7 == 0
valid_days: List[DayOfWeek] = []
for dow in DayOfWeek:
if response_data[2] & (1 << dow.value) == 1:
valid_days.append(dow)
exec_time = timedelta(hours=response_data[3], minutes=response_data[4])
exec_type = AlarmExecType(response_data[5])
exec_action = AlarmExecAction(response_data[6])
action_count = response_data[7]
exec_interval = timedelta(
hours=response_data[8], minutes=response_data[9], seconds=response_data[10]
)
info = AlarmInfo(
exec_repeatedly,
valid_days,
exec_time,
exec_type,
exec_action,
action_count,
exec_interval,
)
self._alarm_count = alarm_count
self._alarm_infos[alarm_idx] = info