Skip to content

Instantly share code, notes, and snippets.

@sroebert
Created March 5, 2023 13:50
Show Gist options
  • Save sroebert/6d6f8071872b50d89119d173361fc66c to your computer and use it in GitHub Desktop.
Save sroebert/6d6f8071872b50d89119d173361fc66c to your computer and use it in GitHub Desktop.
/*global Shelly, MQTT */
/**
* Variables
*/
let FIRMWARE_VERSION;
let MQTT_TOPIC_PREFIX;
let COMPONENT_CONFIG_DICT = {};
let DEVICE_COMPONENT_DICT = {};
let DEVICE_COMPONENT_ARRAY = [];
let DEVICE_INFO = {};
/**
* Utils
*/
function toString(value) {
let type = typeof value;
if (type === "string") {
return value;
}
if (type === "undefined") {
return "";
}
return JSON.stringify(value);
}
function removePrefix(string, prefix) {
if (string.indexOf(prefix) !== 0) {
return string;
}
return string.slice(prefix.length);
}
function splitString(string, separator) {
if (string.length === 0) {
return [];
}
let offset = 0;
let parts = [];
while (offset <= string.length) {
let nextOffset = -1;
if (offset < string.length) {
nextOffset = string.indexOf(separator, offset);
}
if (nextOffset === -1) {
parts.push(string.slice(offset, string.length));
return parts;
}
parts.push(string.slice(offset, nextOffset));
offset = nextOffset + 1;
}
return parts;
}
function joinArray(array, separator) {
if (array.length === 0) {
return "";
}
let result = "";
for (let i = 0; i < array.length - 1; i += 1) {
result += toString(array[i]) + separator;
}
result += toString(array[array.length - 1]);
return result;
}
function parseInstanceIdString(instanceId) {
if (typeof instanceId !== "string" || instanceId.length === 0) {
return undefined;
}
for (let i = 0; i < instanceId.length; i += 1) {
let character = instanceId.at(i);
if (
character < 48 || // <0
character > 57 || // >9
(character === 48 && i === 0 && instanceId.length > 1) // 0[0-9]+
) {
return undefined;
}
}
return JSON.parse(instanceId);
}
function parseComponentId(id) {
if (typeof id !== "string") {
return undefined;
}
let parts = splitString(id, ":");
let type = parts[0];
let instanceId = parseInstanceIdString(parts[1]);
if (
parts.length !== 1 &&
(parts.length !== 2 || typeof instanceId !== "number")
) {
return undefined;
}
let instanceIdString;
if (instanceId !== undefined) {
instanceIdString = toString(instanceId);
}
return {
id: id,
type: type,
instanceId: instanceId,
instanceIdString: instanceIdString,
};
}
function mqttPayloadForPowerValue(value) {
if (typeof value !== "number") {
return undefined;
}
let roundedValue = Math.round(value * 100);
let payload = toString(roundedValue);
while (payload.length < 3) {
payload = "0" + payload;
}
payload = payload.slice(0, -2) + "." + payload.slice(-2);
return payload;
}
function mqttPayloadForTempValue(value) {
if (typeof value !== "object" || typeof value.tC !== "number") {
return undefined;
}
let roundedValue = Math.round(value.tC * 10);
let payload = toString(roundedValue);
while (payload.length < 3) {
payload = "0" + payload;
}
payload = payload.slice(0, -1) + "." + payload.slice(-1);
return payload;
}
/**
* General Data
*/
function onSystemStatusChange(status) {
if (
typeof status.available_updates === "object" &&
status.available_updates.stable !== undefined
) {
if (status.available_updates.stable !== null) {
DEVICE_INFO.firmwareUpdateVersion =
status.available_updates.stable.version;
} else {
DEVICE_INFO.firmwareUpdateVersion = null;
}
} else if (status.available_updates === null) {
DEVICE_INFO.firmwareUpdateVersion = null;
}
}
function onWifiStatusChange(status) {
if (status.sta_ip !== undefined) {
DEVICE_INFO.wifiIp = status.sta_ip;
}
if (status.ssid !== undefined) {
DEVICE_INFO.wifiSsid = status.ssid;
}
}
function publishInfo() {
MQTT.publish(
MQTT_TOPIC_PREFIX + "/info",
JSON.stringify({
wifi_sta: {
connected: DEVICE_INFO.wifiSsid ? true : false,
ip: DEVICE_INFO.wifiIp,
ssid: DEVICE_INFO.wifiSsid,
},
update: {
old_version: FIRMWARE_VERSION,
new_version: DEVICE_INFO.firmwareUpdateVersion
? DEVICE_INFO.firmwareUpdateVersion
: FIRMWARE_VERSION,
},
})
);
}
/**
* Input
*/
function publishInput(status, component) {
if (typeof status.state === "boolean" || status.state === null) {
let payload;
if (status.state === null) {
payload = "null";
} else {
payload = status.state ? "1" : "0";
}
MQTT.publish(
MQTT_TOPIC_PREFIX + "/input/" + component.instanceIdString,
payload
);
}
}
/**
* Switch
*/
function onSwitchCommand(command, component) {
if (command !== "on" && command !== "off") {
return;
}
Shelly.call("Switch.Set", {
id: component.instanceId,
on: command === "on",
});
}
function publishSwitch(status, component) {
let instanceId = component.instanceIdString;
if (typeof status.output === "boolean") {
let payload = status.output ? "on" : "off";
MQTT.publish(MQTT_TOPIC_PREFIX + "/switch/" + instanceId, payload);
}
let powerPayload = mqttPayloadForPowerValue(status.apower);
if (powerPayload !== undefined) {
MQTT.publish(
MQTT_TOPIC_PREFIX + "/switch/" + instanceId + "/power",
powerPayload
);
}
if (
typeof status.aenergy === "object" &&
typeof status.aenergy.total === "number"
) {
let value = Math.round(status.aenergy.total * 60);
let payload = toString(value);
MQTT.publish(
MQTT_TOPIC_PREFIX + "/switch/" + instanceId + "/energy",
payload
);
}
let tempPayload = mqttPayloadForTempValue(status.temperature);
if (tempPayload !== undefined) {
MQTT.publish(
MQTT_TOPIC_PREFIX + "/switch/" + instanceId + "/temperature",
tempPayload
);
}
}
/**
* Energy Meter
*/
function publishEnergyMeter(status, component) {
let instanceId = component.instanceIdString;
let ids = ["a", "b", "c", "total"];
for (let i = 0; i < ids.length; i += 1) {
let id = ids[i];
let key = id + "_act_power";
let payload = mqttPayloadForPowerValue(status[key]);
if (payload !== undefined) {
MQTT.publish(
MQTT_TOPIC_PREFIX + "/energy_meter/" + instanceId + "/power_" + id,
payload
);
}
}
}
function publishEnergyMeterData(status, component) {
let instanceId = component.instanceIdString;
let ids = ["a", "b", "c", "total"];
for (let i = 0; i < ids.length; i += 1) {
let id = ids[i];
let usedKey = id + "_total_act_energy";
let returnedKey = id + "_total_act_ret_energy";
if (id === "total") {
usedKey = "total_act";
returnedKey = "total_act_ret";
}
let usedPayload = mqttPayloadForPowerValue(status[usedKey]);
if (usedPayload !== undefined) {
MQTT.publish(
MQTT_TOPIC_PREFIX + "/energy_meter/" + instanceId + "/energy_" + id,
usedPayload
);
}
let returnedPayload = mqttPayloadForPowerValue(status[returnedKey]);
if (returnedPayload !== undefined) {
MQTT.publish(
MQTT_TOPIC_PREFIX +
"/energy_meter/" +
instanceId +
"/energy_returned_" +
id,
returnedPayload
);
}
}
}
/**
* Events
*/
function onStatusEvent(event) {
if (
typeof event !== "object" ||
typeof event.delta !== "object" ||
typeof event.component !== "string"
) {
return;
}
let component = DEVICE_COMPONENT_DICT[event.component];
if (!component) {
return;
}
let config = COMPONENT_CONFIG_DICT[component.type];
if (config.isPartOfInfo) {
config.changeHandler(event.delta, component);
publishInfo();
} else {
config.publishHandler(event.delta, component);
}
}
/**
* MQTT
*/
function announce() {
MQTT.publish(MQTT_TOPIC_PREFIX + "/online", "true");
publishInfo();
for (let i = 0; i < DEVICE_COMPONENT_ARRAY.length; i += 1) {
let component = DEVICE_COMPONENT_ARRAY[i];
let config = COMPONENT_CONFIG_DICT[component.type];
if (config.isPartOfInfo) {
continue;
}
let status = Shelly.getComponentStatus(component.id);
config.publishHandler(status, component);
}
}
function onMqttCommand(topic, command) {
if (command === "announce") {
announce();
} else if (command === "reboot") {
Shelly.call("Shelly.Reboot", {});
} else if (command === "update_fw") {
Shelly.call("Shelly.Update", {});
}
}
function onComponentMqttCommand(topic, command) {
let suffix = removePrefix(topic, MQTT_TOPIC_PREFIX + "/");
let parts = splitString(suffix, "/");
parts.splice(-1);
let id = joinArray(parts, ":");
let component = DEVICE_COMPONENT_DICT[id];
if (!component) {
return;
}
let config = COMPONENT_CONFIG_DICT[component.type];
if (!config.commandHandler) {
return;
}
config.commandHandler(command, component);
}
function setupMqttSubscriptions() {
MQTT.subscribe("shellies/command", onMqttCommand);
MQTT.subscribe(MQTT_TOPIC_PREFIX + "/command", onMqttCommand);
MQTT.subscribe(MQTT_TOPIC_PREFIX + "/+/command", onComponentMqttCommand);
MQTT.subscribe(MQTT_TOPIC_PREFIX + "/+/+/command", onComponentMqttCommand);
}
function onMqttConnect() {
setupMqttSubscriptions();
announce();
}
/**
* Setup
*/
function configureDevice() {
Shelly.call("Sys.SetConfig", {
config: {
device: {
discoverable: false,
},
},
});
}
function setupConstants() {
let deviceInfo = Shelly.getDeviceInfo();
FIRMWARE_VERSION = deviceInfo.fw_id;
let mqttConfig = Shelly.getComponentConfig("mqtt");
MQTT_TOPIC_PREFIX = mqttConfig.topic_prefix;
}
function setupComponentConfigs() {
let configs = [
{
type: "sys",
isPartOfInfo: true,
changeHandler: onSystemStatusChange,
},
{
type: "wifi",
isPartOfInfo: true,
changeHandler: onWifiStatusChange,
},
{
type: "input",
hasInstanceId: true,
publishHandler: publishInput,
},
{
type: "switch",
hasInstanceId: true,
commandHandler: onSwitchCommand,
publishHandler: publishSwitch,
},
{
type: "em",
hasInstanceId: true,
publishHandler: publishEnergyMeter,
},
{
type: "emdata",
hasInstanceId: true,
publishHandler: publishEnergyMeterData,
},
];
for (let i = 0; i < configs.length; i += 1) {
let config = configs[i];
COMPONENT_CONFIG_DICT[config.type] = config;
}
}
function setupDeviceComponents(deviceStatus) {
for (let id in deviceStatus) {
let component = parseComponentId(id);
if (!component) {
continue;
}
let config = COMPONENT_CONFIG_DICT[component.type];
if (
!config ||
(config.hasInstanceId && typeof component.instanceId === "undefined")
) {
continue;
}
DEVICE_COMPONENT_DICT[id] = component;
DEVICE_COMPONENT_ARRAY.push(component);
if (config.isPartOfInfo) {
config.changeHandler(deviceStatus[id]);
}
}
}
/**
* Start
*/
Shelly.call("Shelly.GetStatus", {}, function (deviceStatus) {
configureDevice();
setupConstants();
setupComponentConfigs();
setupDeviceComponents(deviceStatus);
if (MQTT.isConnected()) {
onMqttConnect();
}
Shelly.addStatusHandler(onStatusEvent);
MQTT.setConnectHandler(onMqttConnect);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment