Commit 3e5765a009 for asterisk.org

commit 3e5765a00921ec5eb6144127071e587280f62a71
Author: Naveen Albert <asterisk@phreaknet.org>
Date:   Mon Sep 21 19:54:42 2026 -0400

    res_manager_devicestate: Add AMI actions to set device/mailbox state.

    Add AMI actions that can be used to set device states and mailbox
    states. This allows these states to be manually set in the same
    way that res_pjsip_publish_asterisk can be used to manually
    synchronize these states. This overcomes some existing limitations:

    * func_devstate only allows setting Custom device states
    * res_mwi_external_ami has a conflict with app_voicemail

    Resolves: #2173

    UserNote: The new DeviceStateChange and MailboxStateChange manager
    actions allow setting device and mailbox states via AMI (the latter
    of which, unlike the existing MWIUpdate action, doesn't require
    res_mwi_external or conflict with app_voicemail).

diff --git a/res/res_manager_devicestate.c b/res/res_manager_devicestate.c
index 6702c29a54..5d5fbf5da7 100644
--- a/res/res_manager_devicestate.c
+++ b/res/res_manager_devicestate.c
@@ -65,6 +65,82 @@
 			</managerEvent>
 		</responses>
 	</manager>
+	<manager name="DeviceStateChange" language="en_US">
+		<since>
+			<version>24.1.0</version>
+			<version>23.7.0</version>
+			<version>22.13.0</version>
+			<version>20.23.0</version>
+		</since>
+		<synopsis>
+			Set a device state
+		</synopsis>
+		<syntax>
+			<xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+			<parameter name="Device" required="true">
+				<para>Device name. Does not need to be Custom; however, set device states for non-Custom devices with caution.</para>
+			</parameter>
+			<parameter name="State" required="true">
+				<para>The new device state value.</para>
+				<para>Should be one of the following:</para>
+				<para>The possible values are:</para>
+				<para>UNKNOWN | NOT_INUSE | INUSE | BUSY | INVALID | UNAVAILABLE | RINGING | RINGINUSE | ONHOLD</para>
+			</parameter>
+			<parameter name="Cachable" required="false">
+				<para>Whether or not this device state is cachable. Default is true, which is needed to persist the update.</para>
+			</parameter>
+			<parameter name="EntityID" required="true">
+				<para>The Entity ID of the remote Asterisk system that originated this device state update.</para>
+				<para>If the Entity ID provided matches the local Asterisk system's Entity ID, the update will be rejected.</para>
+			</parameter>
+		</syntax>
+		<description>
+			<para>Sets a device state value.</para>
+			<para>This can be used to manually synchronize the device state of remote devices.
+			It should NOT be used to set the device state of local devices.</para>
+		</description>
+		<see-also>
+			<ref type="manager">MailboxStateChange</ref>
+		</see-also>
+	</manager>
+	<manager name="MailboxStateChange" language="en_US">
+		<since>
+			<version>24.1.0</version>
+			<version>23.7.0</version>
+			<version>22.13.0</version>
+			<version>20.23.0</version>
+		</since>
+		<synopsis>
+			Set mailbox state
+		</synopsis>
+		<syntax>
+			<xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+			<parameter name="Mailbox" required="true">
+				<para>The name of the mailbox (with optional context).</para>
+			</parameter>
+			<parameter name="NewMessages" required="true">
+				<para>The number of new messages.</para>
+			</parameter>
+			<parameter name="OldMessages" required="true">
+				<para>The number of old messages.</para>
+			</parameter>
+			<parameter name="EntityID" required="true">
+				<para>The Entity ID of the remote Asterisk system that originated this mailbox state update.</para>
+				<para>If the Entity ID provided matches the local Asterisk system's Entity ID, the update will be rejected.</para>
+			</parameter>
+		</syntax>
+		<description>
+			<para>Sets a mailbox's state.</para>
+			<para>This can be used to manually synchronize the mailbox state of remote mailboxes.
+			It should NOT be used to set the mailbox state of local mailboxes.</para>
+			<para>Note this differs from the <literal>MWIUpdate</literal> action, which is more heavyweight
+			and depends on <literal>res_mwi_external</literal>.</para>
+		</description>
+		<see-also>
+			<ref type="manager">DeviceStateChange</ref>
+			<ref type="manager">MWIUpdate</ref>
+		</see-also>
+	</manager>
  ***/


@@ -73,6 +149,8 @@
 #include "asterisk/manager.h"
 #include "asterisk/stasis.h"
 #include "asterisk/devicestate.h"
+#include "asterisk/mwi.h"
+#include "asterisk/conversions.h"

 static struct stasis_forward *topic_forwarder;

@@ -118,10 +196,107 @@ static int action_devicestatelist(struct mansession *s, const struct message *m)
 	return 0;
 }

+static int action_devicestatechange(struct mansession *s, const struct message *m)
+{
+	struct ast_eid eid;
+	enum ast_device_state state_val;
+	const char *device = astman_get_header(m, "Device");
+	const char *state = astman_get_header(m, "State");
+	const char *cachable = astman_get_header(m, "Cachable");
+	const char *entity_id = astman_get_header(m, "EntityID");
+
+	if (ast_strlen_zero(device) || ast_strlen_zero(state)) {
+		astman_send_error(s, m, "Missing device or device state");
+		return 0;
+	}
+	if (ast_strlen_zero(entity_id) || ast_str_to_eid(&eid, entity_id)) {
+		astman_send_error(s, m, "Missing or invalid entity ID");
+		return 0;
+	}
+	if (!strchr(device, '/') && !strchr(device, ':')) {
+		astman_send_error(s, m, "Invalid device name");
+		return 0;
+	}
+
+	state_val = ast_devstate_val(state);
+	/* ast_devstate_val returns AST_DEVICE_UNKNOWN if the device state value is not valid,
+	 * but AST_DEVICE_UNKNOWN is also a legitimate value, so allow it if that's what the input actually was. */
+	if (state_val == AST_DEVICE_UNKNOWN && strcasecmp(state, "UNKNOWN")) {
+		astman_send_error(s, m, "Invalid device state value");
+		return 0;
+	}
+
+	if (!ast_eid_cmp(&ast_eid_default, &eid)) {
+		astman_send_error(s, m, "Entity ID is ourself (must be from a different Asterisk system)");
+		return 0;
+	}
+
+	if (ast_publish_device_state_full(device, state_val, ast_false(cachable) ? AST_DEVSTATE_NOT_CACHABLE : AST_DEVSTATE_CACHABLE, &eid)) {
+		astman_send_error(s, m, "Failed to publish device state update");
+		return 0;
+	}
+
+	astman_send_ack(s, m, "Updated or set device state");
+	return 0;
+}
+
+static int action_mailboxstatechange(struct mansession *s, const struct message *m)
+{
+	struct ast_eid eid;
+	int new_msgs, old_msgs;
+	char *context, *mailbox;
+	const char *mbox = astman_get_header(m, "Mailbox");
+	const char *newmsgs = astman_get_header(m, "NewMessages");
+	const char *oldmsgs = astman_get_header(m, "OldMessages");
+	const char *entity_id = astman_get_header(m, "EntityID");
+
+	if (ast_strlen_zero(mbox) || ast_strlen_zero(newmsgs) || ast_strlen_zero(oldmsgs)) {
+		astman_send_error(s, m, "Missing required parameters");
+		return 0;
+	}
+	if (ast_strlen_zero(entity_id) || ast_str_to_eid(&eid, entity_id)) {
+		astman_send_error(s, m, "Missing or invalid entity ID");
+		return 0;
+	}
+
+	if (ast_str_to_int(newmsgs, &new_msgs) || ast_str_to_int(oldmsgs, &old_msgs)) {
+		astman_send_error(s, m, "Invalid mailbox counts");
+		return 0;
+	}
+	if (new_msgs < 0 || old_msgs < 0) {
+		astman_send_error(s, m, "Invalid mailbox counts (must be non-negative)");
+		return 0;
+	}
+
+	if (!ast_eid_cmp(&ast_eid_default, &eid)) {
+		astman_send_error(s, m, "Entity ID is ourself (must be from a different Asterisk system)");
+		return 0;
+	}
+
+	context = ast_strdupa(mbox);
+	mailbox = strsep(&context, "@");
+
+	/* Mailbox is mandatory, context is optional */
+	if (ast_strlen_zero(mailbox)) {
+		astman_send_error(s, m, "A mailbox is required");
+		return 0;
+	}
+
+	if (ast_publish_mwi_state_full(mailbox, context, new_msgs, old_msgs, NULL, &eid)) {
+		astman_send_error(s, m, "Failed to publish mailbox state update");
+		return 0;
+	}
+
+	astman_send_ack(s, m, "Updated mailbox state");
+	return 0;
+}
+
 static int unload_module(void)
 {
 	topic_forwarder = stasis_forward_cancel(topic_forwarder);
 	ast_manager_unregister("DeviceStateList");
+	ast_manager_unregister("DeviceStateChange");
+	ast_manager_unregister("MailboxStateChange");

 	return 0;
 }
@@ -130,6 +305,11 @@ static int load_module(void)
 {
 	struct stasis_topic *manager_topic;

+	if (ast_eid_is_empty(&ast_eid_default)) {
+		ast_log(LOG_ERROR, "Entity ID is not set.\n");
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
 	manager_topic = ast_manager_get_topic();
 	if (!manager_topic) {
 		return AST_MODULE_LOAD_DECLINE;
@@ -145,6 +325,18 @@ static int load_module(void)
 		return AST_MODULE_LOAD_DECLINE;
 	}

+	if (ast_manager_register_xml("DeviceStateChange", EVENT_FLAG_CALL | EVENT_FLAG_REPORTING, action_devicestatechange)) {
+		topic_forwarder = stasis_forward_cancel(topic_forwarder);
+		ast_manager_unregister("DeviceStateList");
+		return AST_MODULE_LOAD_DECLINE;
+	}
+	if (ast_manager_register_xml("MailboxStateChange", EVENT_FLAG_CALL | EVENT_FLAG_REPORTING, action_mailboxstatechange)) {
+		topic_forwarder = stasis_forward_cancel(topic_forwarder);
+		ast_manager_unregister("DeviceStateList");
+		ast_manager_unregister("DeviceStateChange");
+		return AST_MODULE_LOAD_DECLINE;
+	}
+
 	return AST_MODULE_LOAD_SUCCESS;
 }