Skip to content

Commit

Permalink
only send changed values over mqtt
Browse files Browse the repository at this point in the history
  • Loading branch information
nufke committed Jan 27, 2024
1 parent 877a60f commit 8b337c3
Show file tree
Hide file tree
Showing 41 changed files with 202 additions and 38 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ LoxBerry Plugin for the Philips Air Purifiers and Humidifiers.
* Tested on AC2729/10, not tested on other Philips Air devices.
* Pluging is in early development, use at your own risk.

## Acknowledgements

This plugin is based on [node-red-contrib-philips-airjs](https://github.com/dionmes/node-red-contrib-philips-airjs). Special thanks to [@dionmes](https://github.com/dionmes).
## Credits
* Original reverse engineering of the encrypted-CoAP protocol was done by @rgerganov at https://github.com/rgerganov/py-air-control
* The base of this plugin is made by @dionmes at https://github.com/dionmes/node-red-contrib-philips-airjs.
* Icons are made by @thomasloven and can be found in the repository of @kongo09 at https://github.com/kongo09/philips-airpurifier-coap

## Issues and questions

Expand Down
6 changes: 3 additions & 3 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ const main = () => {
mqttClient.on('message', function(topic, message, packet) {
const device = devices.find(item => topic.includes(item.mqtt));
if (message.length && device) {
let resp = JSON.parse(message.toString());
let resp = message.toString();
const items = topic.split("/");
const command = items[items.length-1]; // select last item
//console.log('command received', command, resp);
logger.debug('Command received via MQTT:' + command + ' ' + resp);
device.inst.sendDeviceCommand(command, resp);
}
});
Expand All @@ -65,7 +65,7 @@ const main = () => {
if (device.ipAddress && device.mqtt) {
logger.info("Registered Philips Air device with IP address " + device.ipAddress);
device.inst = new PhilipsAir(device, logger);
device.inst.on('publishObs', function(topic, value) {
device.inst.on('observation_air', function(topic, value) {
publishTopic(topic,value);
});
} else {
Expand Down
48 changes: 18 additions & 30 deletions bin/lib/philipsAir.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,11 @@ var philipsAir = function(device, logger) {

this.logger.info("Connect to Philips Air at " + this.ipAddress);

this.errorMessage = {
49408: 'no water',
32768: 'water tank open',
49153: "pre-filter must be cleaned",
49155: "pre-filter must be cleaned"
};
this.state = {}; // store current observations

if (this.ipAddress.length != 0) {

// Connect and observe
this.syncAndObserve();

} else {
this.logger.error("IP address not configured.");
}
Expand All @@ -44,16 +37,15 @@ philipsAir.prototype.getTopic = function() {
}

// Function to connect and sync to device and than start observing the device
philipsAir.prototype.syncAndObserve = async function() {
philipsAir.prototype.syncAndObserve = function() {
let that = this;

// Internal function to split response into separate MQTT messages
function splitMqttMessages(resp) {
// TOonly select the relevant to report
const status = ["cl", "func", "mode", "om", "pwr", "iaql", "pm25", "rh", "temp", "wl", "err"];
Object.keys(resp).forEach( key => {
if (status.includes(key)) {
that.emit('publishObs', that.topic + "/" + key, resp[key]);
// only publish changed values
if (resp[key] != that.state[key]) {
that.emit('observation_air', that.topic + "/" + key, resp[key]);
}
});
}
Expand All @@ -69,6 +61,7 @@ philipsAir.prototype.syncAndObserve = async function() {
const resp = json.state.reported;
that.logger.debug('Observed response: ' + JSON.stringify(resp));
splitMqttMessages(resp);
that.state = resp; // keep track of current state
}
}

Expand Down Expand Up @@ -96,10 +89,8 @@ philipsAir.prototype.syncAndObserve = async function() {
};

if (this.observe) {
try {
// Sync device to connect
await this.syncDevice();

// Sync device to connect
this.syncDevice().then( () => {
// Start observing
coap.observe(
url = this.urlPrefix + this.statuspath,
Expand All @@ -110,9 +101,9 @@ philipsAir.prototype.syncAndObserve = async function() {
}).catch(() => {
this.logger.error("Error while observing");
});
} catch (error) {
this.logger.error("Observing Philips Air failed.");
};
}).catch((error) => {
this.logger.error("Observing Philips Air failed with error " + error);
});
}
};

Expand Down Expand Up @@ -149,9 +140,9 @@ philipsAir.prototype.syncDevice = function () {
};

// Function to handle commands/settings to the device
philipsAir.prototype.sendDeviceCommand = async function (commandIn, value) {
philipsAir.prototype.sendDeviceCommand = function (commandIn, value) {
let that = this;
const commands = ["aqil", "cl", "dt", "func", "mode", "om", "pwr", "rhset", "uil"]; // TODO more commands?
const availCommands = ["aqil", "cl", "dt", "func", "mode", "om", "pwr", "rhset", "uil"]; // TODO more commands?

// Internal function to encrypt the message payload
function encryptPayload(unencryptedPayload) {
Expand Down Expand Up @@ -182,12 +173,11 @@ philipsAir.prototype.sendDeviceCommand = async function (commandIn, value) {
return;
}

if (!commands.includes(commandIn.toLowerCase())) {
if (!availCommands.includes(commandIn.toLowerCase())) {
this.logger.error("Command not found.");
return;
}

// TODO split command from full topic
const command = commandIn.toLowerCase();
let commandValue = value.toString().toLowerCase();

Expand All @@ -204,7 +194,6 @@ philipsAir.prototype.sendDeviceCommand = async function (commandIn, value) {
commandValue = parseInt(commandValue);
}

// Create command message
let message = { state: { desired: { CommandType: 'app', DeviceId: '', EnduserId: '1' } } };
message.state.desired[command] = commandValue;

Expand All @@ -214,9 +203,7 @@ philipsAir.prototype.sendDeviceCommand = async function (commandIn, value) {
coap.stopObserving(this.urlPrefix + this.statuspath);

// Sync and then send command
try {
await this.syncDevice();

this.syncDevice().then(() => {
const unencryptedPayload = JSON.stringify(message);
const encryptedPayload = encryptPayload(unencryptedPayload);

Expand All @@ -236,10 +223,11 @@ philipsAir.prototype.sendDeviceCommand = async function (commandIn, value) {
this.logger.error("Command failed to transmit: " + err);
this.syncAndObserve();
});
} catch (err) {
}).catch(err => {
this.logger.error("Philips Air failed to sync, Command failed to transmit: " + err);
this.syncAndObserve();
}
});

};

module.exports = philipsAir;
8 changes: 8 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,11 @@
| uil | Buttons light | string | 0 (off), 1 (on) |
| wicksts | Wick filter replace (hours) | number | |
| wl | Water level | number | 0 (empty), 100 (filled) |

# Error codes

| Error | Description |
|----------|------------------------------|
| 49408 | Water tank empty |
| 32768 | Water tank open |
| 49155 | Clean pre-filter |
40 changes: 40 additions & 0 deletions docs/icons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Icons

The plugin provides the original Philips icons which can be used in your Loxone, LoxBerry or LoxBuddy app. Credit goes to @thomasloven.

| icon | name |
|------------------------------------------------------|-----------------------------|
| ![Preview](../icons/svg/allergen_mode.svg) | allergen_mode.svg |
| ![Preview](../icons/svg/auto_mode.svg) | auto_mode.svg |
| ![Preview](../icons/svg/auto_mode_button.svg) | auto_mode_button.svg |
| ![Preview](../icons/svg/bacteria_virus_mode.svg) | bacteria_virus_mode.svg |
| ![Preview](../icons/svg/child_lock_button.svg) | child_lock_button.svg |
| ![Preview](../icons/svg/circulate.svg) | circulate.svg |
| ![Preview](../icons/svg/clean.svg) | clean.svg |
| ![Preview](../icons/svg/fan_speed_button.svg) | fan_speed_button.svg |
| ![Preview](../icons/svg/filter_replacement.svg) | filter_replacement.svg |
| ![Preview](../icons/svg/gas.svg) | gas.svg |
| ![Preview](../icons/svg/heating.svg) | heating.svg |
| ![Preview](../icons/svg/humidity_button.svg) | humidity_button.svg |
| ![Preview](../icons/svg/iai.svg) | iai.svg |
| ![Preview](../icons/svg/light_dimming_button.svg) | light_dimming_button.svg |
| ![Preview](../icons/svg/mode.svg) | mode.svg |
| ![Preview](../icons/svg/nanoprotect_filter.svg) | nanoprotect_filter.svg |
| ![Preview](../icons/svg/oscillate.svg) | oscillate.svg |
| ![Preview](../icons/svg/pm25.svg) | pm25.svg |
| ![Preview](../icons/svg/pm25b.svg) | pm25b.svg |
| ![Preview](../icons/svg/power_button.svg) | power_button.svg |
| ![Preview](../icons/svg/prefilter_cleaning.svg) | prefilter_cleaning.svg |
| ![Preview](../icons/svg/prefilter_wick_cleaning.svg) | prefilter_wick_cleaning.svg |
| ![Preview](../icons/svg/purification_only_mode.svg) | purification_only_mode.svg |
| ![Preview](../icons/svg/reset.svg) | reset.svg |
| ![Preview](../icons/svg/rotate.svg) | rotate.svg |
| ![Preview](../icons/svg/sleep_mode.svg) | sleep_mode.svg |
| ![Preview](../icons/svg/speed_1.svg) | speed_1.svg |
| ![Preview](../icons/svg/speed_2.svg) | speed_2.svg |
| ![Preview](../icons/svg/speed_3.svg) | speed_3.svg |
| ![Preview](../icons/svg/timer_reset_button.svg) | timer_reset_button.svg |
| ![Preview](../icons/svg/two_in_one_mode.svg) | two_in_one_mode.svg |
| ![Preview](../icons/svg/two_in_one_mode_button.svg) | two_in_one_mode_button |
| ![Preview](../icons/svg/water_refill.svg) | water_refill.svg |
| ![Preview](../icons/svg/wifi.svg) | wifi.svg |
25 changes: 25 additions & 0 deletions icons/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions icons/svg/allergen_mode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions icons/svg/auto_mode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8b337c3

Please sign in to comment.