Skip to content

Commit

Permalink
Merge pull request #43 from markxroberts/await-partition-ready
Browse files Browse the repository at this point in the history
Await partition ready
  • Loading branch information
markxroberts authored Nov 22, 2023
2 parents 3072c51 + 623e880 commit a11b62e
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 26 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This project is a fork of [Johann Vanackere](https://github.com/vanackej/risco-m
## Features

- Interaction with RISCO alarm control panel using local APIs.
- Interaction with MQTT Alarm Control Panel integration in Home Assistant: maps away, home and disarmed.
- Interaction with MQTT Alarm Control Panel integration in Home Assistant: maps away, home and disarmed by default or other states via config.
- Interaction with MQTT Binary Sensor integration in Home Assistant: sensors for each alarm state and additional alarm triggered sensor.
- Home Assistant MQTT Auto Discovery.
- RISCO multipartitions.
Expand All @@ -33,8 +33,11 @@ This project is a fork of [Johann Vanackere](https://github.com/vanackej/risco-m
- Panel connection status/proxy status sensor supported.
- Configurable reconnection delay after dropping of Cloud connection.
- Buttons to republish status, autodiscovery and reinitiate communications.
- Choose whether or not to allow bypass on entry/exit zones (filter_bypass_zones config option)
- System status sensor
- System battery status binary sensor
- Ready status sensor for each partition
- For home arming and group arming, delayed arming introduced in response to partition not ready (otherwise command just fails). This will retry for up to 30 seconds if partition not ready to arm when arming command called. HA alarm control panel will reflect this by showing 'arming'. This is not the same as the Risco 'arming' state which initiates delayed arming (not supported).

## Installation

Expand Down Expand Up @@ -150,21 +153,21 @@ Payload could be : **disarmed** if risco panel is in disarmed mode,**armed_home*

risco-mqtt-local publishes one topic for every partition and for every zones in your risco alarm panel configuration.

Partition topicsformat is `<risco_node_id>/alarm/<partition_id>/status` where **partition_id** is the id of the partition
Partition topics format is `<risco_node_id>/alarm/<partition_id>/status` where **partition_id** is the id of the partition

Payload could be : **disarmed** if risco panel is in disarmed mode,**armed_home** if risco panel is in armed at home mode and **armed_away** if risco panel is in armed away mode.
Default payload could be: **disarmed** if risco panel is in disarmed mode,**armed_home** if risco panel is in armed at home mode and **armed_away** if risco panel is in armed away mode, **armed_custom_bypass** if another mapping has been defined.

Zones topics format is `<risco_node_id>/alarm/<partition_id>/<zone_id>/status` where **partition_id** is the id of the partition and **zone_id** is the id of the zone.

Payload could be : **triggered** if zone is curently triggered, and **idle** if zone is currently idle.

In addition to every zone status, risco-mqtt-local publishes a topic for every zone with all the info of the zone in the payload in json format. Topics format is `<risco_node_id>/alarm/<partition_id>/<zone_id>` where **partition_id** is the id of the partition and **zone_id** is the id of the zone.

Zones that may be bypassed are published as switches at: `<risco_alarm_panel>/alarm/<partition_id>/switch/<zone_id>-bypass`. Entry/exit zones may not be bypassed and so are not published.
Zones that may be bypassed are published as switches at: `<risco_alarm_panel>/alarm/<partition_id>/switch/<zone_id>-bypass`. On some systems, Entry/exit zones may not be bypassed and so you can choose not to publish this by setting the filter_bypass_zones flag.

Battery-powered zones have separate sensors for the battery. These are published at: `<risco_node_id>/alarm/<partition_id>/<zone_id>/battery`. These only have a binary state.

Outputs are published as `<risco_node_id/alarm>/<output_id>/status` for sensor outputs. For outputs where interaction is possible, these are published as `<risco_node_id>/alarm/<output_id>/status` unless buttons, which are stateless. Switches are published to `<risco_node_id>/alarm/<output_id>/set` as subscribed topics.
Outputs are published as `<risco_node_id/alarm>/<output_id>/status` for sensor outputs. For outputs where interaction is possible, these are published as `<risco_node_id>/alarm/<output_id>/status` unless buttons, which are stateless. Switches/buttons are published to `<risco_node_id>/alarm/<output_id>/set` as subscribed topics.

The cloud proxy status is published at `<risco_node_id>/alarm/cloudstatus` if the cloud proxy is enabled.

Expand All @@ -178,7 +181,7 @@ Default `<discovery_prefix>` is **homeassistant**. You can change it by overwrit

Home assistant auto discovery is republished on Home Assistant restart.

For multiple partitions, change **risco_node_id** in each installation.
For multiple partitions, change **risco_mqtt_topic** in each installation.

## Usage

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@markxroberts/risco-mqtt-local",
"version": "2023.11.0",
"version": "2023.11.1",
"description": "Node Risco Mqtt using local panel API",
"main": "dist/main.js",
"types": "dist/lib/index.d.ts",
Expand All @@ -25,7 +25,7 @@
"author": "Mark Roberts <mark.roberts30@gmail.com>",
"license": "MIT",
"dependencies": {
"@markxroberts/risco-lan-bridge": "^0.15.1",
"@markxroberts/risco-lan-bridge": "^0.15.2",
"lodash": "^4.17.21",
"mqtt": "^4.3.2",
"winston": "^3.3.3",
Expand Down
2 changes: 1 addition & 1 deletion risco-mqtt-local-addon/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
FROM markxroberts/risco-mqtt-local:2023.11.0
FROM markxroberts/risco-mqtt-local:2023.11.1
ENV RISCO_MQTT_HA_CONFIG_FILE="/config/risco-mqtt.json"
2 changes: 1 addition & 1 deletion risco-mqtt-local-addon/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Risco local MQTT
version: 2023.11.0
version: 2023.11.1
slug: risco-mqtt-local-addon
description: Risco alarm HA integration using local TCP communication and MQTT
url: https://ghcr.io/markxroberts/risco-mqtt-local
Expand Down
147 changes: 131 additions & 16 deletions src/lib/risco-mqtt-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
let loop;
let reconnect;
let reconnecting = false;
let awaitPartitionReady = false;
let partitionDetailId;
let partitionDetailType;
let partitionReadyStatus = [];
let armingTimer = false

if (!config.mqtt?.url) throw new Error('[RML] MQTT url option is required');

Expand Down Expand Up @@ -381,16 +386,42 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
code = 'armed_group'
}
const group = groupLetterToNumber(letter);
const partStatus = panel.partitions.byId(partId).Ready
logger.debug(`[MQTT => Panel] Changing code for letter. Letter is ${letter}. Group is ${group}.`)
switch (code) {
case 'disarmed':
return await panel.disarmPart(partId);
case 'armed_home':
return await panel.armHome(partId);
if (partitionReadyStatus[partId] === true) {
logger.info(`[RML] Partition ${partId} ready, sending arm command`)
logger.debug(`${partitionReadyStatus[partId]}`)
return await panel.armHome(partId);
} else {
awaitPartitionReady = true
partitionDetailId = partId
partitionDetailType = code
logger.info(`[RML] Partition ${partId} not ready. Will await Ready status.`)
logger.debug(`${partitionReadyStatus[partId]}`)
}
case 'armed_away':
return await panel.armAway(partId);
try {
return await panel.armAway(partId);
}
catch (error) {
logger.info(`${error}`)
}
case 'armed_group':
return await panel.armGroup(partId,group);
if (partitionReadyStatus[partId] === true) {
logger.info(`[RML] Partition ${partId} ready, sending arm command`)
logger.debug(`${partitionReadyStatus[partId]}`)
return await panel.armGroup(partId, group);
} else {
awaitPartitionReady = true
partitionDetailId = partId
partitionDetailType = code
logger.info(`[RML] Partition ${partId} not ready. Will await Ready status.`)
logger.debug(`${partitionReadyStatus[partId]}`)
}
}
}

Expand Down Expand Up @@ -449,14 +480,14 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
text: EventStr};
}
} else {
if (output.OStatus === 'a') {
if (output.Active) {
return {
output: '1',
text: 'Activated'};
text: output.Active};
} else {
return {
output: '0',
text: 'Deactivated'};
text: output.Active};
}
}
}
Expand Down Expand Up @@ -537,9 +568,35 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
logger.verbose(`[Panel => MQTT] Published system battery state ${message}`);
}

function publishPartitionStateChanged(partition: Partition) {
mqttClient.publish(`${config.risco_mqtt_topic}/alarm/partition/${partition.Id}/status`, alarmPayload(partition), { qos: 1, retain: true });
logger.verbose(`[Panel => MQTT] Published alarm status ${alarmPayload(partition)} on partition ${partition.Id}`);
function partitionStatus(partition: Partition) {
if (partition.Ready) {
return {
state: '0',
text: 'Ready'
};
} else {
return {
state: '1',
text: 'Not ready'
};
}
}

function publishPartitionStateChanged(partition: Partition, arming: boolean) {
if (!arming) {
mqttClient.publish(`${config.risco_mqtt_topic}/alarm/partition/${partition.Id}/status`, alarmPayload(partition), { qos: 1, retain: true });
logger.verbose(`[Panel => MQTT] Published alarm status ${alarmPayload(partition)} on partition ${partition.Id}`);
}
if (arming) {
mqttClient.publish(`${config.risco_mqtt_topic}/alarm/partition/${partition.Id}/status`, 'arming', { qos: 1, retain: true });
logger.verbose(`[Panel => MQTT] Published alarm status arming on partition ${partition.Id}`);
}
}

function publishPartitionStatus(partition: Partition) {
const partitionState = partitionStatus(partition)
mqttClient.publish(`${config.risco_mqtt_topic}/alarm/partition/${partition.Id}-status/status`, partitionState.state, { qos: 1, retain: true });
logger.verbose(`[Panel => MQTT] Published partition status ${partitionState.text} on partition ${partition.Id}`);
}

function publishZoneStateChange(zone: Zone, publishAttributes: boolean) {
Expand Down Expand Up @@ -837,6 +894,9 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {

for (const partition of activePartitions(panel.partitions)) {

partitionReadyStatus.push({[partition.Id]: partition.Ready})
logger.debug(`Partition status on ${partition.Id} is ${partition.Ready}`)

const partitionConf = cloneDeep(config.partitions.default);
merge(partitionConf, config.partitions?.[partition.Label]);

Expand All @@ -856,7 +916,7 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
}};
alarmMapping.push(alarmRemap);
logger.info(`[RML] Added alarm state mapping for partition ${partitionLabel}.`)
logger.verbose(`[RML] Added alarm state mappings for parition ${partitionLabel} as \n${JSON.stringify(alarmRemap, null, 2)}.`)
logger.verbose(`[RML] Added alarm state mappings for partition ${partitionLabel} as \n${JSON.stringify(alarmRemap, null, 2)}.`)
logger.verbose(`[RML] Alarm mappings updated as \n${JSON.stringify(alarmMapping, null, 2)}.`)

const payload = {
Expand All @@ -883,11 +943,35 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {

let partitionIdSegment = `${partition.Id}`;

let partitionSensorName = `${partition.Label} status`

const partitionpayload = {
name: partitionSensorName,
object_id: `${config.risco_mqtt_topic}-${partition.Id}-status`,
state_topic: `${config.risco_mqtt_topic}/alarm/partition/${partition.Id}-status/status`,
unique_id: `${config.risco_mqtt_topic}-partition-${partition.Id}-status`,
availability_mode: 'all',
availability: [
{topic: `${config.risco_mqtt_topic}/alarm/status`},
{topic: `${config.risco_mqtt_topic}/alarm/button_status`}],
payload_on: '1',
payload_off: '0',
device_class: 'occupancy',
device: getDeviceInfo(),
};

partitionpayload.name = partitionConf.name_prefix + partitionName;

mqttClient.publish(`${config.ha_discovery_prefix_topic}/alarm_control_panel/${config.risco_mqtt_topic}/${partitionIdSegment}/config`, JSON.stringify(payload), {
qos: 1, retain: true,
});
logger.info(`[Panel => MQTT][Discovery] Published alarm_control_panel to HA Partition label = ${partition.Label}, HA name = ${payload.name} on partition ${partition.Id}`);
logger.verbose(`[Panel => MQTT][Discovery] Alarm discovery payload\n${JSON.stringify(payload, null, 2)}`);
mqttClient.publish(`${config.ha_discovery_prefix_topic}/binary_sensor/${config.risco_mqtt_topic}/partition-${partitionIdSegment}-status/config`, JSON.stringify(partitionpayload), {
qos: 1, retain: true,
});
logger.info(`[Panel => MQTT][Discovery] Published binary_sensor of partition status to HA label = ${partition.Label}, HA name = ${partitionpayload.name} on partition ${partition.Id}`);
logger.verbose(`[Panel => MQTT][Discovery] Partition status sensor discovery payload\n${JSON.stringify(partitionpayload, null, 2)}`);
}

for (const output of activeToggleOutputs(panel.outputs)) {
Expand Down Expand Up @@ -1118,7 +1202,8 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
function publishInitialStates() {
logger.info(`[RML] Publishing initial partitions, zones and outputs states to Home assistant`);
for (const partition of activePartitions(panel.partitions)) {
publishPartitionStateChanged(partition);
publishPartitionStateChanged(partition, false);
publishPartitionStatus(partition);
}
for (const zone of activeZones(panel.zones)) {
publishZoneStateChange(zone, true);
Expand All @@ -1145,7 +1230,33 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {

function partitionListener(Id, EventStr) {
if (['Armed', 'Disarmed', 'HomeStay', 'HomeDisarmed', 'Alarm', 'StandBy', 'GrpAArmed', 'GrpBArmed', 'GrpCArmed', 'GrpDArmed', 'GrpADisarmed', 'GrpBDisarmed', 'GrpCDisarmed', 'GrpDDisarmed'].includes(EventStr)) {
publishPartitionStateChanged(panel.partitions.byId(Id));
publishPartitionStateChanged(panel.partitions.byId(Id), false);
}
if (['Ready', 'NotReady'].includes(EventStr)) {
let partitionwait
publishPartitionStatus(panel.partitions.byId(Id));
if (['Ready'].includes(EventStr)) {
partitionReadyStatus[Id] = true
if (awaitPartitionReady) {
logger.info(`[RML] Partition ${Id} now ready, so sending arming command.`)
clearTimeout(partitionwait);
changeAlarmStatus(partitionDetailType, partitionDetailId);
awaitPartitionReady = false
armingTimer = false
}
} else {
partitionReadyStatus[Id] = false;
if (awaitPartitionReady && !armingTimer) {
publishPartitionStateChanged(panel.partitions.byId(Id), true);
armingTimer = true
partitionwait = setTimeout(function() {
awaitPartitionReady = false;
armingTimer = false
logger.info(`[RML] Arming command timed out on partition ${Id}`)}, 30000)
} if (armingTimer) {
logger.info(`[RML] Delayed arming already initiated on partition ${Id}.`)
}
}
}
}

Expand All @@ -1157,7 +1268,7 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
publishZoneBypassStateChange(panel.zones.byId(Id));
publishZoneStateChange(panel.zones.byId(Id), true);
}
if (['LowBattery', 'BatteryOK'].includes(EventStr)) {
if (['LowBattery', 'BatteryOk'].includes(EventStr)) {
publishZoneStateChange(panel.zones.byId(Id), true);
publishZoneBatteryStateChange(panel.zones.byId(Id), false);
}
Expand All @@ -1183,8 +1294,12 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {

function errorListener(type, data) {
logger.info(`[RML] Error received ${type}, ${data}`);
logger.info('[Panel => MQTT] Panel not communicating properly. Panel offline');
publishPanelStatus(false);
logger.info('[RML] Panel or cloud not communicating properly.');
if (data.includes('Cloud')) {
publishPanelStatus(true);
} else {
publishPanelStatus(false)
}
if (type.includes('CommsError')) {
if (data.includes('New socket being connected')) {
logger.info('[RML] TCP Socket disconnected, new socket being connected. Ensure old listeners removed.');
Expand All @@ -1202,7 +1317,7 @@ export function riscoMqttHomeAssistant(userConfig: RiscoMQTTConfig) {
panelReady = false;
logger.info(`[RML] Panel unreachable.`)
reconnecting = true;
} else if (data.includes('Cloud socket Closed' || 'RiscoCloud Socket: close' || 'Risco command error: TIMEOUT')) {
} else if (data.includes('Cloud socket Closed' || 'RiscoCloud Socket: closed' || 'Risco command error: TIMEOUT')) {
logger.info(`[RML] Cloud socket error ${data} received. Disconnecting socket to avoid reconnection loop.`)
panelReady = false;
socketDisconnected(true);
Expand Down

0 comments on commit a11b62e

Please sign in to comment.