Skip to content

Commit

Permalink
#165 EVO control doors
Browse files Browse the repository at this point in the history
  • Loading branch information
yozik04 committed Jul 22, 2020
1 parent 7e932e5 commit 29ce8a5
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/pai.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import logging
# MQTT_DEFINITIONS_TOPIC = 'control' # Base for definitions
# MQTT_HOMEASSISTANT_DISCOVERY_PREFIX = 'homeassistant'
# MQTT_OUTPUT_TOPIC = 'outputs'
# MQTT_DOOR_TOPIC = 'doors'
# MQTT_KEYPAD_TOPIC = 'keypads'
# MQTT_STATES_TOPIC = 'states'
# MQTT_RAW_TOPIC = 'raw'
Expand Down
1 change: 1 addition & 0 deletions paradox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class Config(object):
"MQTT_HOMEASSISTANT_CONTROL_TOPIC": "hass_control",
"MQTT_HOMEASSISTANT_DISCOVERY_PREFIX": "homeassistant",
"MQTT_OUTPUT_TOPIC": "outputs",
"MQTT_DOOR_TOPIC": "doors",
"MQTT_KEYPAD_TOPIC": "keypads",
"MQTT_STATES_TOPIC": "states",
"MQTT_RAW_TOPIC": "raw",
Expand Down
24 changes: 24 additions & 0 deletions paradox/hardware/evo/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,30 @@ async def control_outputs(self, outputs, command) -> bool:
logger.info('PGM command: "%s" failed' % command)
return reply is not None

async def control_doors(self, doors, command) -> bool:
"""
Control Doors
:param list doors: a list of doors
:param str command: textual command
:return: True if we have at least one success
"""

args = {"doors": doors, "command": command}

try:
reply = await self.core.send_wait(
parsers.PerformDoorAction, args, reply_expected=0x04
)
except MappingError:
logger.error('Door command: "%s" is not supported' % command)
return False

if reply:
logger.info('Door command: "%s" succeeded' % command)
else:
logger.info('Door command: "%s" failed' % command)
return reply is not None

async def send_panic(self, partition, panic_type, user_id):
accepted = False

Expand Down
4 changes: 4 additions & 0 deletions paradox/hardware/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ def control_partitions(self, partitions, command) -> bool:
def control_outputs(self, outputs, command) -> bool:
raise NotImplementedError("override control_outputs in a subclass")

@abstractmethod
def control_doors(self, doors, command) -> bool:
raise NotImplementedError("override control_doors in a subclass")

@abstractmethod
def dump_memory(self, file, memory_type):
raise NotImplementedError("override dump_memory in a subclass")
Expand Down
21 changes: 21 additions & 0 deletions paradox/interfaces/mqtt/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ def on_connect(self, mqttc, userdata, flags, result):
),
self._mqtt_handle_output_control,
)
self.subscribe_callback(
"{}/{}/{}/#".format(
cfg.MQTT_BASE_TOPIC, cfg.MQTT_CONTROL_TOPIC, cfg.MQTT_DOOR_TOPIC
),
self._mqtt_handle_door_control,
)
self.subscribe_callback(
"{}/{}/{}/#".format(
cfg.MQTT_BASE_TOPIC, cfg.MQTT_CONTROL_TOPIC, cfg.MQTT_ZONE_TOPIC
Expand Down Expand Up @@ -256,6 +262,21 @@ async def _mqtt_handle_send_panic(self, prep: ParsedMessage):
logger.warning("Send panic command refused: {}, user: {}, type: {}".format(partition, userid, panic_type))


@mqtt_handle_decorator
async def _mqtt_handle_door_control(self, prep: ParsedMessage):
topics, element, command = prep

if cfg.MQTT_CHALLENGE_SECRET is not None:
command = self._validate_command_with_challenge(command)

if command is None:
return

logger.debug("Door command: {} = {}".format(element, command))

if not await self.alarm.control_door(element, command):
logger.warning("Door command refused: {}={}".format(element, command))

def _handle_panel_event(self, event: Event):
"""
Handle Live Event
Expand Down
28 changes: 28 additions & 0 deletions paradox/paradox.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,34 @@ async def send_panic(self, partition_id, panic_type, user_id) -> bool:
except asyncio.TimeoutError:
logger.error("send_panic timeout")

async def control_door(self, door, command) -> bool:
command = command.lower()
logger.debug("Control Door: {} - {}".format(door, command))

doors_selected = self.storage.get_container("door").select(door)

# Not Found
if len(doors_selected) == 0:
logger.error("No doors selected")
return False

# Apply state changes
accepted = False
try:
accepted = await self.panel.control_doors(doors_selected, command)
except NotImplementedError:
logger.error("control_door is not implemented for this alarm type")
except asyncio.CancelledError:
logger.error("control_door canceled")
except asyncio.TimeoutError:
logger.error("control_door timeout")
# Apply state changes

# Refresh status
self.request_status_refresh() # Trigger status update

return accepted


def get_label(self, label_type: str, label_id) -> Optional[str]:
el = self.storage.get_container_object(label_type, label_id)
Expand Down
20 changes: 20 additions & 0 deletions tests/interfaces/mqtt/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ async def test_mqtt_handle_zone_control_utf8(mocker):
interface.join()
assert not interface.is_alive()


@pytest.mark.asyncio
async def test_mqtt_handle_send_panic(mocker):
interface = get_interface(mocker)
Expand All @@ -157,3 +158,22 @@ async def test_mqtt_handle_send_panic(mocker):
interface.stop()
interface.join()
assert not interface.is_alive()


@pytest.mark.asyncio
async def test_mqtt_handle_door_control(mocker):
interface = get_interface(mocker)
try:
await asyncio.sleep(0.01)

message = MQTTMessage(topic="paradox/control/doors/Door 1".encode("utf-8"))
message.payload = b"unlock"

interface._mqtt_handle_door_control(None, None, message)

await asyncio.sleep(0.01)
interface.alarm.control_door.assert_called_once_with("Door 1", "unlock")
finally:
interface.stop()
interface.join()
assert not interface.is_alive()
13 changes: 13 additions & 0 deletions tests/test_paradox.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,17 @@ async def test_send_panic(mocker):
alarm.panel.send_panic.assert_called_once_with(1, "fire", 3)


@pytest.mark.asyncio
async def test_control_doors(mocker):
alarm = Paradox()
alarm.panel = mocker.Mock(spec=Panel)
alarm.panel.control_doors = CoroutineMock()

alarm.storage.get_container("door").deep_merge({1: {"id": 1, "key": "Door 1"}})

assert await alarm.control_door("1", "unlock")
alarm.panel.control_doors.assert_called_once_with([1], "unlock")
alarm.panel.control_doors.reset_mock()

assert await alarm.control_door("Door 1", "unlock")
alarm.panel.control_doors.assert_called_once_with([1], "unlock")

0 comments on commit 29ce8a5

Please sign in to comment.