Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
yozik04 committed Oct 28, 2024
2 parents 96436f6 + 9673ea0 commit 583fb1f
Show file tree
Hide file tree
Showing 27 changed files with 410 additions and 306 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
uses: ./.github/workflows/test.yml
publish:
name: Publish to Docker Hub
if: github.repository == 'ParadoxAlarmInterface/pai' && github.ref == 'refs/heads/dev'
uses: ./.github/workflows/publish_docker.yml
needs: test
if: github.repository_owner == 'ParadoxAlarmInterface'
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ jobs:
name: Publish to Docker Hub
uses: ./.github/workflows/publish_docker.yml
needs: test
if: github.repository_owner == 'ParadoxAlarmInterface'
if: github.repository == 'ParadoxAlarmInterface/pai'
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
publish_pypi:
name: Publish to PyPI
uses: ./.github/workflows/publish_pypi.yml
needs: test
if: github.repository_owner == 'ParadoxAlarmInterface'
if: github.repository == 'ParadoxAlarmInterface/pai'
secrets:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
rev: v3.17.0
hooks:
- id: pyupgrade
args: ["--py37-plus"]

- repo: https://github.com/psf/black
rev: 24.4.0
rev: 24.8.0
hooks:
- id: black
args:
Expand All @@ -35,7 +35,7 @@ repos:
- id: isort

- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ Can be looked up in Babyware (_Right click on a panel ⇾ Properties ⇾ PC Comm
We do not recommend using SWAN because of https://github.com/CriticalSecurity/paradox

## Firmware Upgrade WARNING:
**Do not upgrade EVO firmware versions to 7.50.000+ if you use Serial connection. Process is irreversible! Paradox introduces serial communication encryption which most probably will break our PAI ability to talk to the panel.**
**Do not upgrade EVO firmware versions to 7.50.000+ if you use Serial connection. Process is irreversible! Paradox introduces serial communication encryption which most probably will break our PAI ability to talk to the panel.**
Note: Paradox sells unlock code to re-enable the unencrypted serial port.

## How to use
See [wiki](https://github.com/ParadoxAlarmInterface/pai/wiki/Installation)
Expand Down
19 changes: 11 additions & 8 deletions config/pai.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ import logging
### MQTT Home Assistant Auto Discovery
# MQTT_HOMEASSISTANT_AUTODISCOVERY_ENABLE = False
# MQTT_HOMEASSISTANT_CODE = None
# HOMEASSISTANT_PUBLISH_PARTITION_PROPERTIES = [
# 'target_state',
# 'current_state'
# ]
# HOMEASSISTANT_PUBLISH_ZONE_PROPERTIES = [
# 'open',
# 'tamper'
# ]
#
### Dash App
# MQTT_DASH_PUBLISH = False
Expand All @@ -137,16 +145,11 @@ import logging
#
### Home Assistant Notifications (HASS.io required)
# HOMEASSISTANT_NOTIFICATIONS_ENABLE = False
# HOMEASSISTANT_NOTIFICATIONS_API_URL = "http://supervisor/core/api/services/:domain/:service"
# HOMEASSISTANT_NOTIFICATIONS_API_TOKEN = "" # Long-Lived Access Token. Required if you do not use HA Supervisor
# HOMEASSISTANT_NOTIFICATIONS_LOVELACE_URI = "" # URI to open when notification is clicked
# HOMEASSISTANT_NOTIFICATIONS_NOTIFIER_NAME = 'notify'
# HOMEASSISTANT_NOTIFICATIONS_MIN_EVENT_LEVEL = 'INFO'
# HOMEASSISTANT_PUBLISH_PARTITION_PROPERTIES = [
# 'target_state',
# 'current_state'
# ]
# HOMEASSISTANT_PUBLISH_ZONE_PROPERTIES = [
# 'open',
# 'tamper'
# ]
## Event filtering by tags:
# HOMEASSISTANT_NOTIFICATIONS_EVENT_FILTERS = [ # list of tags to include or exclude see hardware event.py for tag list
# 'live,alarm,-restore', # or
Expand Down
19 changes: 11 additions & 8 deletions paradox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,6 @@ class Config:
"armed_away": "arm",
"disarmed": "disarm",
},
# Home Assistant Notifications (HASS.io required)
"HOMEASSISTANT_NOTIFICATIONS_ENABLE": False,
"HOMEASSISTANT_NOTIFICATIONS_NOTIFIER_NAME": "notify",
"HOMEASSISTANT_NOTIFICATIONS_MIN_EVENT_LEVEL": (
"INFO",
str,
["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"],
),
"HOMEASSISTANT_PUBLISH_PARTITION_PROPERTIES": [ # List of partition properties to publish
"target_state",
"current_state",
Expand All @@ -180,6 +172,17 @@ class Config:
"open",
"tamper",
],
# Home Assistant Notifications (HASS.io required)
"HOMEASSISTANT_NOTIFICATIONS_ENABLE": False,
"HOMEASSISTANT_NOTIFICATIONS_API_URL": "http://supervisor/core/api/services/:domain/:service",
"HOMEASSISTANT_NOTIFICATIONS_API_TOKEN": "", # Authentication token used for Home Assistant if not using Supervisor
"HOMEASSISTANT_NOTIFICATIONS_LOVELACE_URI": "", # URI to open when notification is clicked
"HOMEASSISTANT_NOTIFICATIONS_NOTIFIER_NAME": "notify",
"HOMEASSISTANT_NOTIFICATIONS_MIN_EVENT_LEVEL": (
"INFO",
str,
["DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"],
),
"HOMEASSISTANT_NOTIFICATIONS_IGNORE_EVENTS": [], # List of tuples or regexp matching "type,label,property=value,property2=value" eg. [(major, minor), "zone:HOME:entry_delay=True", ...]
"HOMEASSISTANT_NOTIFICATIONS_ALLOW_EVENTS": [], # Same as before but as a white list. Default is use EVENT_FILTERS
"HOMEASSISTANT_NOTIFICATIONS_EVENT_FILTERS": [ # list of tags, property changes to include or exclude. See event.py for tag list
Expand Down
10 changes: 6 additions & 4 deletions paradox/connections/ip/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(self, host="127.0.0.1", port=10000):
self.port = port

async def _try_connect(self):
_, self._protocol = await self.loop.create_connection(
_, self._protocol = await asyncio.get_event_loop().create_connection(
self._make_protocol, host=self.host, port=self.port
)

Expand Down Expand Up @@ -90,7 +90,9 @@ def set_key(self, value):
self._protocol.key = value

def on_ip_message(self, container: Container):
return self.loop.create_task(self.ip_handler_registry.handle(container))
return asyncio.get_event_loop().create_task(
self.ip_handler_registry.handle(container)
)

async def wait_for_ip_message(self, timeout=cfg.IO_TIMEOUT) -> Container:
future = FutureHandler()
Expand All @@ -115,7 +117,7 @@ def __init__(
self.port = port

async def _try_connect(self) -> None:
_, self._protocol = await self.loop.create_connection(
_, self._protocol = await asyncio.get_event_loop().create_connection(
self._make_protocol, host=self.host, port=self.port
)

Expand Down Expand Up @@ -146,7 +148,7 @@ def write(self, data: bytes):

async def _try_connect(self) -> None:
await self.stun_session.connect()
_, self._protocol = await self.loop.create_connection(
_, self._protocol = await asyncio.get_event_loop().create_connection(
self._make_protocol, sock=self.stun_session.get_socket()
)

Expand Down
15 changes: 6 additions & 9 deletions paradox/connections/serial_connection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-


import asyncio
import logging
import os
import stat
import typing

import serial_asyncio
from serial import SerialException
import serial_asyncio

from ..exceptions import SerialConnectionOpenFailed
from .connection import Connection
Expand Down Expand Up @@ -67,12 +64,12 @@ async def connect(self) -> bool:
logger.error(f"Failed to update file {self.port_path} permissions")
return False

self.connected_future = self.loop.create_future()
open_timeout_handler = self.loop.call_later(5, self.open_timeout)
self.connected_future = asyncio.get_event_loop().create_future()
open_timeout_handler = asyncio.get_event_loop().call_later(5, self.open_timeout)

try:
_, self._protocol = await serial_asyncio.create_serial_connection(
self.loop, self.make_protocol, self.port_path, self.baud
asyncio.get_event_loop(), self.make_protocol, self.port_path, self.baud
)

return await self.connected_future
Expand All @@ -81,7 +78,7 @@ async def connect(self) -> bool:
raise SerialConnectionOpenFailed(
"Connection to serial port failed"
) from e # PAICriticalException
except:
except Exception:
logger.exception("Unable to connect to Serial")
finally:
open_timeout_handler.cancel()
Expand Down
6 changes: 6 additions & 0 deletions paradox/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ class PAICriticalException(PAIException):
class AuthenticationFailed(PAICriticalException):
pass


class CodeLockout(PAICriticalException):
pass


class PanelNotDetected(PAICriticalException):
pass

Expand All @@ -46,6 +48,10 @@ class SerialConnectionOpenFailed(PAICriticalException):
pass


class InvalidCommand(PAIException):
pass


def async_loop_unhandled_exception_handler(loop, context):
exception = context.get("exception")

Expand Down
54 changes: 27 additions & 27 deletions paradox/interfaces/text/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

from paradox.config import config as cfg
from paradox.event import Event, EventLevel, Notification
from paradox.interfaces import ThreadQueueInterface
from paradox.exceptions import InvalidCommand
from paradox.interfaces import AsyncInterface
from paradox.lib import ps
from paradox.lib.event_filter import (EventFilter, EventTagFilter,
LiveEventRegexpFilter)
from paradox.lib.event_filter import EventFilter, EventTagFilter, LiveEventRegexpFilter

logger = logging.getLogger("PAI").getChild(__name__)


class AbstractTextInterface(ThreadQueueInterface):
class AbstractTextInterface(AsyncInterface):
"""Interface Class using any Text interface"""

def __init__(self, alarm, event_filter: EventFilter, min_level=EventLevel.INFO):
Expand All @@ -21,12 +21,7 @@ def __init__(self, alarm, event_filter: EventFilter, min_level=EventLevel.INFO):
self.min_level = min_level
self.alarm = alarm

def stop(self):
super().stop()

def _run(self):
super(AbstractTextInterface, self)._run()

async def run(self):
ps.subscribe(self.handle_panel_event, "events")
ps.subscribe(self.handle_notify, "notifications")

Expand All @@ -38,19 +33,25 @@ def notification_filter(self, notification: Notification):

def handle_notify(self, notification: Notification):
if self.notification_filter(notification):
self.send_message(notification.message, notification.level)
try:
self.send_message(notification.message, notification.level)
except Exception as e:
logger.exception(f"Error handling notification: {e}")

def handle_panel_event(self, event: Event):
if self.event_filter.match(event):
self.send_message(event.message, event.level)
try:
self.send_message(event.message, event.level)
except Exception as e:
logger.exception(f"Error handling event: {e}")

async def handle_command(self, message_raw):
message = cfg.COMMAND_ALIAS.get(message_raw, message_raw)

tokens = message.split(" ")

if len(tokens) != 3:
m = "Invalid: {}".format(message_raw)
m = f"Invalid: {message_raw}"
logger.warning(m)
return m

Expand All @@ -61,46 +62,45 @@ async def handle_command(self, message_raw):

element_type = tokens[0].lower()
element = tokens[1]
command = self.normalize_payload(tokens[2].lower())
command = self.normalize_command(tokens[2].lower())

# Process a Zone Command
if element_type == "zone":
if not await self.alarm.control_zone(element, command):
m = "Zone command error: {}={}".format(element, command)
m = f"Zone command error: {element}={command}"
logger.warning(m)
return m

# Process a Partition Command
elif element_type == "partition":
if not await self.alarm.control_partition(element, command):
m = "Partition command error: {}={}".format(element, command)
m = f"Partition command error: {element}={command}"
logger.warning(m)
return m

# Process an Output Command
elif element_type == "output":
if not await self.alarm.control_output(element, command):
m = "Output command error: {}={}".format(element, command)
m = f"Output command error: {element}={command}"
logger.warning(m)
return m
else:
m = "Invalid control element: {}".format(element)
m = f"Invalid control element: {element}"
logger.error(m)
return m

logger.info("OK: {}".format(message_raw))
logger.info(f"OK: {message_raw}")
return "OK"

# TODO: Remove this (to panels?)
@staticmethod
def normalize_payload(message):
message = message.strip().lower()
def normalize_command(command):
command = command.strip().lower()

if message in ["true", "on", "1", "enable"]:
if command in ["true", "on", "1", "enable"]:
return "on"
elif message in ["false", "off", "0", "disable"]:
elif command in ["false", "off", "0", "disable"]:
return "off"
elif message in [
elif command in [
"pulse",
"arm",
"disarm",
Expand All @@ -109,9 +109,9 @@ def normalize_payload(message):
"bypass",
"clear_bypass",
]:
return message
return command

return None
raise InvalidCommand(f'Invalid command: "{command}"')


class ConfiguredAbstractTextInterface(AbstractTextInterface):
Expand Down
Loading

0 comments on commit 583fb1f

Please sign in to comment.