Skip to content

Commit

Permalink
fix: command execution
Browse files Browse the repository at this point in the history
Access properties correctly
  • Loading branch information
muniter committed Jul 25, 2024
1 parent 70fac2f commit 7a3466c
Showing 1 changed file with 103 additions and 51 deletions.
154 changes: 103 additions & 51 deletions halinuxcompanion/notifier.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from halinuxcompanion.companion import Companion
from halinuxcompanion.companion import CommandConfig, Companion
from halinuxcompanion.api import API, Server
from halinuxcompanion.dbus import Dbus

Expand Down Expand Up @@ -40,16 +40,15 @@
}

RESPONSES = {
"invalid_token":
json.dumps({
"error": "push_token does not match",
"errorMessage": "Sent token that does not match to halinuxcompaion munrig"
}).encode('ascii'),
"ok":
json.dumps({
"success": True,
"message": "Notification queued"
}).encode('ascii'),
"invalid_token": json.dumps(
{
"error": "push_token does not match",
"errorMessage": "Sent token that does not match to halinuxcompaion munrig",
}
).encode("ascii"),
"ok": json.dumps({"success": True, "message": "Notification queued"}).encode(
"ascii"
),
}

EMPTY_DICT = {}
Expand All @@ -65,21 +64,26 @@ class Notifier:
6. When dbus events are generated, it emits the event to Home Assistant (if appropieate).
7. Some action events perform a local action like opening a url.
"""

# Only keeping the last 20 notifications and popping everytime a new one is added
history: OrderedDict[int, dict] = OrderedDict((x, EMPTY_DICT) for x in range(-1, -21, -1))
history: OrderedDict[int, dict] = OrderedDict(
(x, EMPTY_DICT) for x in range(-1, -21, -1)
)
tagtoid: Dict[str, int] = {} # Lookup id from tag
interface: ProxyInterface
api: API
push_token: str
url_program: str
commands: Dict[str, dict]
commands: Dict[str, CommandConfig]
ha_url: str

def __init__(self):
# The initialization is done in the init function
pass

async def init(self, dbus: Dbus, api: API, webserverver: Server, companion: Companion) -> None:
async def init(
self, dbus: Dbus, api: API, webserverver: Server, companion: Companion
) -> None:
"""Function to initialize the notifier.
1. Gets the dbus interface to send notifications and listen to events.
2. Registers an http handler to the webserver for Home Assistant notifications.
Expand All @@ -94,7 +98,9 @@ async def init(self, dbus: Dbus, api: API, webserverver: Server, companion: Comp
interface = await dbus.get_interface("org.freedesktop.Notifications")

if interface is None:
logger.warning("Could not find org.freedesktop.Notifications interface, disabling notification support.")
logger.warning(
"Could not find org.freedesktop.Notifications interface, disabling notification support."
)
return

self.interface = interface
Expand All @@ -103,7 +109,7 @@ async def init(self, dbus: Dbus, api: API, webserverver: Server, companion: Comp
self.interface.on_notification_closed(self.on_close)

# Setup http server route handler for incoming notifications
webserverver.app.router.add_route('POST', '/notify', self.on_ha_notification)
webserverver.app.router.add_route("POST", "/notify", self.on_ha_notification)

# API and necessary data
self.api = api
Expand All @@ -129,34 +135,48 @@ async def on_ha_notification(self, request) -> Response:

# Check if the notification is for this device
if push_token != self.push_token:
logger.error("Notification push_token does not match: %s != %s", push_token, self.push_token)
logger.error(
"Notification push_token does not match: %s != %s",
push_token,
self.push_token,
)
return json_response(body=RESPONSES["invalid_token"], status=400)

# Transform the notification to the format dbus uses
notification = self.notification_transform(notification)

if notification["is_command"]:
command_id = notification["message"]
command = self.commands.get(command_id, {})
if self.commands and command:
command = self.commands.get(command_id)
if command:
# It's not a notification, but a command, therefore no dbus_notify
name = command["name"]
command_args = command["command"]
logger.info("Received notification command: id:%s name:%s", command_id, name)
logger.info("Scheduling notification command: %s", command_args)
logger.info(
"Received notification command: id:%s name:%s", command_id, command.name
)
logger.info("Scheduling notification command: %s", command.command)
asyncio.create_task(
asyncio.create_subprocess_exec(*command_args,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL))
asyncio.create_subprocess_exec(
*command.command,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
)
else:
# Got notificatoin command but none defined
logger.error("Received notification command %s, but no command is defined", command_id)
logger.error(
"Received notification command %s, but no command is defined",
command_id,
)
else:
asyncio.create_task(self.dbus_notify(self.notification_transform(notification)))
asyncio.create_task(
self.dbus_notify(self.notification_transform(notification))
)

return json_response(body=RESPONSES["ok"], status=201)

async def ha_event_trigger(self, event: str, action: str = "", notification: dict = {}) -> bool:
async def ha_event_trigger(
self, event: str, action: str = "", notification: dict = {}
) -> bool:
"""Function to trigger the Home Assistant event given an event type and notification dictionary.
Actions are first handled in on_action which decides wether to emit the event or not.
Expand All @@ -183,7 +203,12 @@ async def ha_event_trigger(self, event: str, action: str = "", notification: dic

try:
res = await self.api.post(endpoint, json.dumps(data))
logger.info("Sent Home Assistant event:%s data:%s response:%s", endpoint, data, res.status)
logger.info(
"Sent Home Assistant event:%s data:%s response:%s",
endpoint,
data,
res.status,
)
return True
except ClientError as e:
logger.error("Error sending Home Assistant event: %s", e)
Expand Down Expand Up @@ -219,7 +244,9 @@ def notification_transform(self, notification: dict) -> dict:
# https://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#basic-design

# Dbus notification structure [id, name, id, name, ...]
event_actions = {} # Format the actions as necessary for on_close an on_action events
event_actions = (
{}
) # Format the actions as necessary for on_close an on_action events
counter = 1
for a in data.get("actions", []):
actions.extend([a["action"], a["title"]])
Expand Down Expand Up @@ -261,15 +288,17 @@ def notification_transform(self, notification: dict) -> dict:
# Replace the notification and hide it in 1 millisecond, workaround for dbus notifications
timeout = 1

notification.update({
"title": notification.get("title", HA),
"actions": actions,
"hints": hints,
"timeout": timeout,
"icon": icon, # TODO: Support custom icons
"replace_id": replace_id,
"is_command": False,
})
notification.update(
{
"title": notification.get("title", HA),
"actions": actions,
"hints": hints,
"timeout": timeout,
"icon": icon, # TODO: Support custom icons
"replace_id": replace_id,
"is_command": False,
}
)
logger.debug("Converted notification: %s", notification)

return notification
Expand All @@ -285,9 +314,16 @@ async def dbus_notify(self, notification: dict) -> None:
:return: None
"""
logger.info("Sending dbus notification")
id = await self.interface.call_notify(APP_NAME, notification["replace_id"], notification["icon"],
notification["title"], notification["message"], notification["actions"],
notification["hints"], notification["timeout"])
id = await self.interface.call_notify(
APP_NAME,
notification["replace_id"],
notification["icon"],
notification["title"],
notification["message"],
notification["actions"],
notification["hints"],
notification["timeout"],
)
logger.info("Dbus notification dispatched id:%s", id)

# History management: Add the new notification, and remove the oldest one.
Expand All @@ -311,10 +347,14 @@ async def on_action(self, id: int, action: str) -> None:
:param id: The dbus id of the notification
:param action: The action that was invoked
"""
logger.info("Notification action dbus event received: id:%s, action:%s", id, action)
logger.info(
"Notification action dbus event received: id:%s, action:%s", id, action
)
notification: dict = self.history.get(id, {})
if not notification:
logger.info("No notification found for id:%s, doesn't belong to this applicaton", id)
logger.info(
"No notification found for id:%s, doesn't belong to this applicaton", id
)
return

actions: List[dict] = notification["data"].get("actions", {})
Expand All @@ -326,14 +366,20 @@ async def on_action(self, id: int, action: str) -> None:
uri = notification.get("default_action_uri", "")
emit_event = False
else:
uri = next(filter(lambda dic: dic["action"] == action, actions)).get("uri", "")
uri = next(filter(lambda dic: dic["action"] == action, actions)).get(
"uri", ""
)

if uri.startswith("http") and self.url_program != "":
asyncio.create_task(asyncio.create_subprocess_exec(self.url_program, uri))
asyncio.create_task(
asyncio.create_subprocess_exec(self.url_program, uri)
)
logger.info("Launched action:%s uri:%s", action, uri)

if emit_event:
asyncio.create_task(self.ha_event_trigger("action", action, notification))
asyncio.create_task(
self.ha_event_trigger("action", action, notification)
)

async def on_close(self, id: int, reason: str) -> None:
"""Function to handle the dbus notification close event
Expand All @@ -342,9 +388,15 @@ async def on_close(self, id: int, reason: str) -> None:
:param id: The dbus id of the notification
:param reason: The reason the notification was closed
"""
logger.info("Notification closed dbus event received: id:%s, reason:%s", id, reason)
logger.info(
"Notification closed dbus event received: id:%s, reason:%s", id, reason
)
notification = self.history.get(id, {})
if notification:
asyncio.create_task(self.ha_event_trigger(event="closed", notification=notification))
asyncio.create_task(
self.ha_event_trigger(event="closed", notification=notification)
)
else:
logger.info("No notification found for id:%s, doesn't belong to this applicaton", id)
logger.info(
"No notification found for id:%s, doesn't belong to this applicaton", id
)

0 comments on commit 7a3466c

Please sign in to comment.