Skip to content

Commit

Permalink
Merge pull request #51 from hostcc/feat/support-setting-sensor-user-flag
Browse files Browse the repository at this point in the history
feat: Support setting user flags on sensors
  • Loading branch information
hostcc authored Dec 29, 2024
2 parents 8aad8d3 + a11524d commit 16b5645
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 18 deletions.
68 changes: 51 additions & 17 deletions src/pyg90alarm/entities/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ class G90SensorUserFlags(IntFlag):
ALERT_WHEN_AWAY_AND_HOME = 32
ALERT_WHEN_AWAY = 64
SUPPORTS_UPDATING_SUBTYPE = 512 # Only relevant for cord sensors
# Flags that can be set by the user
USER_SETTABLE = (
ENABLED
| ARM_DELAY
| DETECT_DOOR
| DOOR_CHIME
| INDEPENDENT_ZONE
| ALERT_WHEN_AWAY_AND_HOME
| ALERT_WHEN_AWAY
)


class G90SensorProtocols(IntEnum):
Expand Down Expand Up @@ -487,18 +497,26 @@ def enabled(self) -> bool:
"""
return self.user_flag & G90SensorUserFlags.ENABLED != 0

async def set_enabled(self, value: bool) -> None:
async def set_user_flag(self, value: G90SensorUserFlags) -> None:
"""
Sets disabled/enabled state of the sensor.
Sets user flags of the sensor.
:param value: Whether to enable or disable the sensor
:param value: User flags to set, values other than
:attr:`.G90SensorUserFlags.USER_SETTABLE` will be ignored and
preserved from existing sensor flags.
"""
if value & ~G90SensorUserFlags.USER_SETTABLE:
_LOGGER.warning(
'User flags for sensor index=%s contain non-user settable'
' flags, those will be ignored: %s',
self.index, repr(value & ~G90SensorUserFlags.USER_SETTABLE)
)
# Checking private attribute directly, since `mypy` doesn't recognize
# the check for sensor definition to be defined if done over
# `self.supports_enable_disable` property
if not self._definition:
_LOGGER.warning(
'Manipulating with enable/disable for sensor index=%s'
'Manipulating with user flags for sensor index=%s'
' is unsupported - no sensor definition for'
' type=%s, subtype=%s',
self.index,
Expand Down Expand Up @@ -526,7 +544,7 @@ async def set_enabled(self, value: bool) -> None:
if not sensors:
_LOGGER.error(
'Sensor index=%s not found when attempting to set its'
' enable/disable state',
' user flag',
self.index,
)
return
Expand All @@ -539,30 +557,30 @@ async def set_enabled(self, value: bool) -> None:
) != self._protocol_data:
_LOGGER.error(
"Sensor index=%s '%s' has been changed externally,"
" refusing to alter its enable/disable state",
" refusing to alter its user flag",
self.index,
self.name
)
return

# Modify the value of the user flag setting enabled/disabled one
# appropriately.
user_flag = self.user_flag
if value:
user_flag |= G90SensorUserFlags.ENABLED
else:
user_flag &= ~G90SensorUserFlags.ENABLED
prev_user_flag = self.user_flag

# Re-instantiate the protocol data with modified user flags
_data = asdict(self._protocol_data)
_data['user_flag_data'] = user_flag
_data['user_flag_data'] = (
# Preserve flags that are not user-settable
self.user_flag & ~G90SensorUserFlags.USER_SETTABLE
) | (
# Combine them with the new user-settable flags
value & G90SensorUserFlags.USER_SETTABLE
)
self._protocol_data = self._protocol_incoming_data_kls(**_data)

_LOGGER.debug(
'Sensor index=%s: %s enabled flag, resulting user_flag %s',
'Sensor index=%s: previous user_flag %s, resulting user_flag %s',
self._protocol_data.index,
'Setting' if value else 'Clearing',
self.user_flag
repr(prev_user_flag),
repr(self.user_flag)
)

# Generate protocol data from write operation, deriving values either
Expand All @@ -589,6 +607,20 @@ async def set_enabled(self, value: bool) -> None:
G90Commands.SETSINGLESENSOR, list(astuple(outgoing_data))
)

async def set_enabled(self, value: bool) -> None:
"""
Sets the sensor enabled/disabled.
"""

# Modify the value of the user flag setting enabled/disabled one
# appropriately.
user_flag = self.user_flag
if value:
user_flag |= G90SensorUserFlags.ENABLED
else:
user_flag &= ~G90SensorUserFlags.ENABLED
await self.set_user_flag(user_flag)

@property
def extra_data(self) -> Any:
"""
Expand Down Expand Up @@ -623,6 +655,8 @@ def _asdict(self) -> Dict[str, Any]:
'supports_enable_disable': self.supports_enable_disable,
'is_wireless': self.is_wireless,
'is_low_battery': self.is_low_battery,
'is_tampered': self.is_tampered,
'is_door_open_when_arming': self.is_door_open_when_arming,
}

def __repr__(self) -> str:
Expand Down
35 changes: 34 additions & 1 deletion tests/test_alarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
G90HostInfo, G90HostInfoGsmStatus, G90HostInfoWifiStatus,
)
from pyg90alarm.entities.sensor import (
G90Sensor,
G90Sensor, G90SensorUserFlags,
)
from pyg90alarm.entities.device import (
G90Device,
Expand Down Expand Up @@ -1099,3 +1099,36 @@ async def test_device_unsupported_disable(mock_device: DeviceMock) -> None:
assert mock_device.recv_data == [
b'ISTART[138,138,[138,[1,10]]]IEND\0',
]


@pytest.mark.g90device(sent_data=[
b'ISTART[102,'
b'[[1,1,1],'
b'["Night Light2",10,0,138,0,0,33,0,0,17,1,0,""]'
b']]IEND\0',
b'ISTART[102,'
b'[[1,1,1],'
b'["Night Light2",10,0,138,0,0,33,0,0,17,1,0,""]'
b']]IEND\0',
b"ISTARTIEND\0",
])
async def test_sensor_set_user_flags(mock_device: DeviceMock) -> None:
"""
Tests for setting user flags on a sensor.
"""
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
sensors = await g90.get_sensors()
await sensors[0].set_user_flag(
# Intentionally contains non-user settable flag, which should be
# ignored and not configured for the sensor that initial doesn't have
# it set
G90SensorUserFlags.INDEPENDENT_ZONE | G90SensorUserFlags.ARM_DELAY
| G90SensorUserFlags.SUPPORTS_UPDATING_SUBTYPE
)
assert mock_device.recv_data == [
b'ISTART[102,102,[102,[1,10]]]IEND\0',
b'ISTART[102,102,[102,[1,1]]]IEND\0',
b'ISTART[103,103,[103,'
b'["Night Light2",10,0,138,0,0,18,0,0,17,1,0,2,"060A0600"]'
b']]IEND\0',
]

0 comments on commit 16b5645

Please sign in to comment.