Skip to content

Commit 029f88b

Browse files
committed
refactor: Create a separate endpoint for device triggers
refactor!: Normalize device action endpoint BREAKING CHANGE: Endpoint for device action changed location from nodered/device_action to nodered/device/action It now matches the new device trigger format
1 parent 1c8a782 commit 029f88b

File tree

4 files changed

+102
-123
lines changed

4 files changed

+102
-123
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ _Companion Component to [node-red-contrib-home-assistant-websocket](https://gith
2222

2323
## Minimum Requirements
2424

25-
- [node-red-contrib-home-assistant-websocket](https://github.com/zachowj/node-red-contrib-home-assistant-websocket) v0.20+
25+
- [node-red-contrib-home-assistant-websocket](https://github.com/zachowj/node-red-contrib-home-assistant-websocket) v0.57+
2626
- [Home Assistant](https://github.com/home-assistant/core) 2023.7.0+
2727

2828
## Installation

custom_components/nodered/const.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
CONF_ENABLED = "enabled"
2121
CONF_ENTITY_PICTURE = "entity_picture"
2222
CONF_LAST_RESET = "last_reset"
23+
CONF_MESSAGE = "message"
2324
CONF_NAME = "name"
2425
CONF_NODE_ID = "node_id"
2526
CONF_NUMBER = "number"
2627
CONF_OPTIONS = "options"
2728
CONF_OUTPUT_PATH = "output_path"
28-
CONF_PAYLOAD = "payload"
2929
CONF_REMOVE = "remove"
3030
CONF_SELECT = "select"
3131
CONF_SENSOR = "sensor"

custom_components/nodered/switch.py

+7-107
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
"""Sensor platform for nodered."""
2-
import json
1+
"""Switch platform for nodered."""
32
import logging
43

54
from homeassistant.components.websocket_api import event_message
@@ -9,11 +8,9 @@
98
CONF_ID,
109
CONF_STATE,
1110
CONF_TYPE,
12-
EVENT_HOMEASSISTANT_START,
1311
EVENT_STATE_CHANGED,
1412
)
15-
from homeassistant.core import CoreState, callback
16-
from homeassistant.helpers import entity_platform, trigger
13+
from homeassistant.helpers import entity_platform
1714
import homeassistant.helpers.config_validation as cv
1815
from homeassistant.helpers.dispatcher import async_dispatcher_connect
1916
from homeassistant.helpers.entity import ToggleEntity
@@ -23,30 +20,23 @@
2320
from .const import (
2421
CONF_CONFIG,
2522
CONF_DATA,
26-
CONF_DEVICE_TRIGGER,
23+
CONF_MESSAGE,
2724
CONF_OUTPUT_PATH,
28-
CONF_PAYLOAD,
29-
CONF_REMOVE,
30-
CONF_SKIP_CONDITION,
31-
CONF_SUB_TYPE,
3225
CONF_SWITCH,
3326
CONF_TRIGGER_ENTITY_ID,
34-
DOMAIN,
3527
NODERED_DISCOVERY_NEW,
3628
SERVICE_TRIGGER,
3729
SWITCH_ICON,
3830
)
39-
from .utils import NodeRedJSONEncoder
4031

4132
_LOGGER = logging.getLogger(__name__)
4233

4334
SERVICE_TRIGGER_SCHEMA = vol.Schema(
4435
{
4536
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
4637
vol.Optional(CONF_TRIGGER_ENTITY_ID): cv.entity_id,
47-
vol.Optional(CONF_SKIP_CONDITION): cv.boolean,
4838
vol.Optional(CONF_OUTPUT_PATH): cv.boolean,
49-
vol.Optional(CONF_PAYLOAD): vol.Extra,
39+
vol.Optional(CONF_MESSAGE): vol.Extra,
5040
}
5141
)
5242
EVENT_TRIGGER_NODE = "automation_triggered"
@@ -78,11 +68,7 @@ async def async_discover(config, connection):
7868
async def _async_setup_entity(hass, config, async_add_entities, connection):
7969
"""Set up the Node-RED Switch."""
8070

81-
switch_type = config.get(CONF_SUB_TYPE, TYPE_SWITCH)
82-
switch_class = (
83-
NodeRedDeviceTrigger if switch_type == TYPE_DEVICE_TRIGGER else NodeRedSwitch
84-
)
85-
async_add_entities([switch_class(hass, config, connection)])
71+
async_add_entities([NodeRedSwitch(hass, config, connection)])
8672

8773

8874
class NodeRedSwitch(NodeRedEntity, ToggleEntity):
@@ -116,11 +102,9 @@ async def async_turn_on(self, **kwargs) -> None:
116102
async def async_trigger_node(self, **kwargs) -> None:
117103
"""Trigger node in Node-RED."""
118104
data = {}
119-
data[CONF_ENTITY_ID] = kwargs.get(CONF_TRIGGER_ENTITY_ID)
120-
data[CONF_SKIP_CONDITION] = kwargs.get(CONF_SKIP_CONDITION, False)
121105
data[CONF_OUTPUT_PATH] = kwargs.get(CONF_OUTPUT_PATH, True)
122-
if kwargs.get(CONF_PAYLOAD) is not None:
123-
data[CONF_PAYLOAD] = kwargs[CONF_PAYLOAD]
106+
if kwargs.get(CONF_MESSAGE) is not None:
107+
data[CONF_MESSAGE] = kwargs[CONF_MESSAGE]
124108

125109
self._connection.send_message(
126110
event_message(
@@ -145,87 +129,3 @@ def update_discovery_config(self, msg):
145129
"""Update the entity config."""
146130
super().update_discovery_config(msg)
147131
self._attr_icon = msg[CONF_CONFIG].get(CONF_ICON, SWITCH_ICON)
148-
149-
150-
class NodeRedDeviceTrigger(NodeRedSwitch):
151-
"""Node-RED Device Trigger class."""
152-
153-
def __init__(self, hass, config, connection):
154-
"""Initialize the switch."""
155-
super().__init__(hass, config, connection)
156-
self._trigger_config = config[CONF_DEVICE_TRIGGER]
157-
self._unsubscribe_device_trigger = None
158-
159-
@callback
160-
def handle_lost_connection(self):
161-
"""Set remove device trigger when disconnected."""
162-
super().handle_lost_connection()
163-
self.remove_device_trigger()
164-
165-
async def add_device_trigger(self):
166-
"""Add device trigger."""
167-
168-
if self.hass.state == CoreState.running:
169-
await self._attach_triggers()
170-
else:
171-
self._unsub_start = self.hass.bus.async_listen_once(
172-
EVENT_HOMEASSISTANT_START, self._attach_triggers
173-
)
174-
175-
async def _attach_triggers(self, start_event=None) -> None:
176-
try:
177-
trigger_config = await trigger.async_validate_trigger_config(
178-
self.hass, [self._trigger_config]
179-
)
180-
self._unsubscribe_device_trigger = await trigger.async_initialize_triggers(
181-
self.hass,
182-
trigger_config,
183-
self.forward_trigger,
184-
DOMAIN,
185-
DOMAIN,
186-
_LOGGER.log,
187-
)
188-
except vol.MultipleInvalid as ex:
189-
_LOGGER.error(
190-
f"Error initializing device trigger '{self._node_id}': {str(ex)}",
191-
)
192-
193-
@callback
194-
def forward_trigger(self, event, context=None):
195-
"""Forward events to websocket."""
196-
message = event_message(
197-
self._message_id,
198-
{"type": EVENT_DEVICE_TRIGGER, "data": event["trigger"]},
199-
)
200-
self._connection.send_message(
201-
json.dumps(message, cls=NodeRedJSONEncoder, allow_nan=False)
202-
)
203-
204-
def remove_device_trigger(self):
205-
"""Remove device trigger."""
206-
self._trigger_config = None
207-
if self._unsubscribe_device_trigger is not None:
208-
_LOGGER.info(f"removed device triger - {self._server_id} {self._node_id}")
209-
self._unsubscribe_device_trigger()
210-
self._unsubscribe_device_trigger = None
211-
212-
@callback
213-
async def handle_discovery_update(self, msg, connection):
214-
"""Update entity config."""
215-
if CONF_REMOVE not in msg and self._trigger_config != msg[CONF_DEVICE_TRIGGER]:
216-
self.remove_device_trigger()
217-
self._trigger_config = msg[CONF_DEVICE_TRIGGER]
218-
await self.add_device_trigger()
219-
220-
super().handle_discovery_update(msg, connection)
221-
222-
async def async_added_to_hass(self):
223-
"""Run when entity about to be added to hass."""
224-
await super().async_added_to_hass()
225-
226-
await self.add_device_trigger()
227-
228-
async def async_will_remove_from_hass(self) -> None:
229-
"""Run when entity will be removed from hass."""
230-
self.remove_device_trigger()
231-
await super().async_will_remove_from_hass()

custom_components/nodered/websocket.py

+93-14
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
CONF_WEBHOOK_ID,
3737
)
3838
from homeassistant.core import HomeAssistant, callback
39-
from homeassistant.helpers import config_validation as cv, device_registry as dr
39+
from homeassistant.helpers import (
40+
config_validation as cv,
41+
device_registry as dr,
42+
trigger,
43+
)
4044
from homeassistant.helpers.dispatcher import async_dispatcher_send
4145
from homeassistant.helpers.entity_registry import async_entries_for_device, async_get
4246
from homeassistant.helpers.typing import HomeAssistantType
@@ -58,6 +62,7 @@
5862
NODERED_ENTITY,
5963
VERSION,
6064
)
65+
from .utils import NodeRedJSONEncoder
6166

6267
CONF_ALLOWED_METHODS = "allowed_methods"
6368
CONF_LOCAL_ONLY = "local_only"
@@ -70,6 +75,7 @@ def register_websocket_handlers(hass: HomeAssistantType):
7075

7176
async_register_command(hass, websocket_device_action)
7277
async_register_command(hass, websocket_device_remove)
78+
async_register_command(hass, websocket_device_trigger)
7379
async_register_command(hass, websocket_discovery)
7480
async_register_command(hass, websocket_entity)
7581
async_register_command(hass, websocket_config_update)
@@ -81,7 +87,7 @@ def register_websocket_handlers(hass: HomeAssistantType):
8187
@require_admin
8288
@websocket_command(
8389
{
84-
vol.Required(CONF_TYPE): "nodered/device_action",
90+
vol.Required(CONF_TYPE): "nodered/device/action",
8591
vol.Required("action"): cv.DEVICE_ACTION_SCHEMA,
8692
}
8793
)
@@ -97,13 +103,15 @@ async def websocket_device_action(
97103

98104
try:
99105
await platform.async_call_action_from_config(hass, msg["action"], {}, context)
100-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
106+
connection.send_message(result_message(msg[CONF_ID]))
101107
except InvalidDeviceAutomationConfig as err:
102108
connection.send_message(error_message(msg[CONF_ID], "invalid_config", str(err)))
103109
except DeviceNotFound as err:
104110
connection.send_message(
105111
error_message(msg[CONF_ID], "device_not_found", str(err))
106112
)
113+
except Exception as err:
114+
connection.send_message(error_message(msg[CONF_ID], "unknown_error", str(err)))
107115

108116

109117
@require_admin
@@ -131,7 +139,7 @@ async def websocket_device_remove(
131139

132140
device_registry.async_remove_device(device.id)
133141

134-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
142+
connection.send_message(result_message(msg[CONF_ID]))
135143

136144

137145
@require_admin
@@ -157,7 +165,7 @@ def websocket_discovery(
157165
async_dispatcher_send(
158166
hass, NODERED_DISCOVERY.format(msg[CONF_COMPONENT]), msg, connection
159167
)
160-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
168+
connection.send_message(result_message(msg[CONF_ID]))
161169

162170

163171
@require_admin
@@ -178,7 +186,7 @@ def websocket_entity(
178186
async_dispatcher_send(
179187
hass, NODERED_ENTITY.format(msg[CONF_SERVER_ID], msg[CONF_NODE_ID]), msg
180188
)
181-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
189+
connection.send_message(result_message(msg[CONF_ID]))
182190

183191

184192
@require_admin
@@ -198,7 +206,7 @@ def websocket_config_update(
198206
async_dispatcher_send(
199207
hass, NODERED_CONFIG_UPDATE.format(msg[CONF_SERVER_ID], msg[CONF_NODE_ID]), msg
200208
)
201-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
209+
connection.send_message(result_message(msg[CONF_ID]))
202210

203211

204212
@require_admin
@@ -260,7 +268,7 @@ def remove_webhook() -> None:
260268
pass
261269

262270
_LOGGER.info(f"Webhook removed: {webhook_id[:15]}..")
263-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
271+
connection.send_message(result_message(msg[CONF_ID]))
264272

265273
try:
266274
hass.components.webhook.async_register(
@@ -270,13 +278,16 @@ def remove_webhook() -> None:
270278
handle_webhook,
271279
allowed_methods=allowed_methods,
272280
)
273-
except ValueError:
274-
connection.send_message(result_message(msg[CONF_ID], {"success": False}))
281+
except ValueError as err:
282+
connection.send_message(error_message(msg[CONF_ID], "value_error", str(err)))
283+
return
284+
except Exception as err:
285+
connection.send_message(error_message(msg[CONF_ID], "unknown_error", str(err)))
275286
return
276287

277288
_LOGGER.info(f"Webhook created: {webhook_id[:15]}..")
278289
connection.subscriptions[msg[CONF_ID]] = remove_webhook
279-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
290+
connection.send_message(result_message(msg[CONF_ID]))
280291

281292

282293
@require_admin
@@ -322,10 +333,78 @@ def remove_trigger() -> None:
322333
assert isinstance(default_agent, DefaultAgent)
323334

324335
_remove_trigger = default_agent.register_trigger(sentences, handle_trigger)
325-
except ValueError:
326-
connection.send_message(result_message(msg[CONF_ID], {"success": False}))
336+
except ValueError as err:
337+
connection.send_message(error_message(msg[CONF_ID], "value_error", str(err)))
338+
return
339+
except Exception as err:
340+
connection.send_message(error_message(msg[CONF_ID], "unknown_error", str(err)))
327341
return
328342

329343
_LOGGER.info(f"Sentence trigger created: {sentences}")
330344
connection.subscriptions[msg[CONF_ID]] = remove_trigger
331-
connection.send_message(result_message(msg[CONF_ID], {"success": True}))
345+
connection.send_message(result_message(msg[CONF_ID]))
346+
347+
348+
@require_admin
349+
@websocket_command(
350+
{
351+
vol.Required(CONF_TYPE): "nodered/device/trigger",
352+
vol.Required(CONF_NODE_ID): cv.string,
353+
vol.Required(CONF_DEVICE_TRIGGER, default={}): dict,
354+
}
355+
)
356+
@async_response
357+
async def websocket_device_trigger(
358+
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
359+
) -> None:
360+
"""Create device trigger."""
361+
node_id = msg[CONF_NODE_ID]
362+
trigger_data = msg[CONF_DEVICE_TRIGGER]
363+
364+
def forward_trigger(event, context=None):
365+
"""Forward events to websocket."""
366+
message = event_message(
367+
msg[CONF_ID],
368+
{"type": "device_trigger", "data": event["trigger"]},
369+
)
370+
connection.send_message(
371+
json.dumps(message, cls=NodeRedJSONEncoder, allow_nan=False)
372+
)
373+
374+
def unsubscribe() -> None:
375+
"""Remove device trigger."""
376+
remove_trigger()
377+
_LOGGER.info(f"Device trigger removed: {node_id}")
378+
379+
try:
380+
trigger_config = await trigger.async_validate_trigger_config(
381+
hass, [trigger_data]
382+
)
383+
remove_trigger = await trigger.async_initialize_triggers(
384+
hass,
385+
trigger_config,
386+
forward_trigger,
387+
DOMAIN,
388+
DOMAIN,
389+
_LOGGER.log,
390+
)
391+
392+
except vol.MultipleInvalid as err:
393+
_LOGGER.error(
394+
f"Error initializing device trigger '{node_id}': {str(err)}",
395+
)
396+
connection.send_message(
397+
error_message(msg[CONF_ID], "invalid_trigger", str(err))
398+
)
399+
return
400+
except Exception as err:
401+
_LOGGER.error(
402+
f"Error initializing device trigger '{node_id}': {str(err)}",
403+
)
404+
connection.send_message(error_message(msg[CONF_ID], "unknown_error", str(err)))
405+
return
406+
407+
_LOGGER.info(f"Device trigger created: {node_id}")
408+
_LOGGER.debug(f"Device trigger config for {node_id}: {trigger_data}")
409+
connection.subscriptions[msg[CONF_ID]] = unsubscribe
410+
connection.send_message(result_message(msg[CONF_ID]))

0 commit comments

Comments
 (0)