feat(shelly): add terraform_shelly workspace for Taupi-4.0 dew point ventilation
This commit is contained in:
parent
768f7c83f7
commit
85bfabd387
|
|
@ -0,0 +1,13 @@
|
|||
# Local .terraform directories
|
||||
**/.terraform/*
|
||||
|
||||
# .tfstate files
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
|
||||
# Crash log files
|
||||
crash.log
|
||||
crash.*.log
|
||||
|
||||
# Exclude templated script outputs
|
||||
scripts/Taupi-4.0.templated.js
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/donrobo/shelly" {
|
||||
version = "0.4.1"
|
||||
constraints = "~> 0.4.1"
|
||||
hashes = [
|
||||
"h1:NxND2178u+OfQgiKtDr+m+IDCAHAyfrzQGM+TurzfcY=",
|
||||
"zh:494c3185a84cd9534da78e3a32b1523301835afdf9ada921675c7739f4565256",
|
||||
"zh:53597cbe0826b662933adddb65d7adfb952d0ea91c13d14aaa49c549273af671",
|
||||
"zh:5841856412803f8d5d6873aa1da61cb34efd6ce2f4a569f82c9edda078aa7b74",
|
||||
"zh:5a9835a6b7ef703b0e56a5cdd7b0843510561c25da366eb57c7758a0d90c8079",
|
||||
"zh:660251598da4c8196d88421eccd5db1451136165f800e41a0b11dbb722eabfe1",
|
||||
"zh:6d1e9b227d2f2088d25a90066137e2f0d93300028f00acf4b1febb80f333fb3f",
|
||||
"zh:80ac9b9ca8d1afd9615778e7cb20d66da24b98bdbc7d6ec41508b9414247a07f",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:8cf57de63467e62a1378cb3a268adeb2489e7b654eeff3a57039b880d1fb9c95",
|
||||
"zh:9702ece653dc97c707c480ba25c9a7807f595a27887dbac032cec0972bff7f44",
|
||||
"zh:989ec7dcd21d2aec7f389aa1f3536c8501e2b5773f6c3b9ad9d6e5d8f232767e",
|
||||
"zh:aebef49c4efa03d05d2454041c670d5aa22f4707c62c685bd647437b0844ed7c",
|
||||
"zh:af2c5947276b00d514e7e94eda840e3382234b0ee1fc4a6f6990d704f6bf0e57",
|
||||
"zh:fdffcb420d380c80ca9464e9907e65c65600f0571e0081194caf4f4aae6460ca",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/local" {
|
||||
version = "2.9.0"
|
||||
constraints = "~> 2.4"
|
||||
hashes = [
|
||||
"h1:oF4vw1rikPMqQtMMBoUjc/pERp50DlWwO+C2I6/sNI8=",
|
||||
"zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0",
|
||||
"zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64",
|
||||
"zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90",
|
||||
"zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c",
|
||||
"zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8",
|
||||
"zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8",
|
||||
"zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b",
|
||||
"zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9",
|
||||
"zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195",
|
||||
"zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3",
|
||||
"zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/null" {
|
||||
version = "3.3.0"
|
||||
constraints = "~> 3.2"
|
||||
hashes = [
|
||||
"h1:Gq+xRK4yvwDpBJ2KFJkrmcNZikDiAEuFhAWeDtzanJc=",
|
||||
"zh:021748b5ea3b5f6956f2e75c42c5cdc113b391fb98ac71364a4965d23b37000f",
|
||||
"zh:3b27956f8541d46704fda234e0d535c2ae2a4b33411848b1ee262a1ec03568b0",
|
||||
"zh:3de4ed47d6d0f4d8edba4a5092c7c9799950eda63989d8d0d2586e6afcb0aa20",
|
||||
"zh:57ed8935c7d56dbc91cf2673534582cacfaab7a2f105f51d9f797e99df0c0c47",
|
||||
"zh:58e176ba1d142827089e30e0711e007309a9f2726e8881986da5026e9778fdf4",
|
||||
"zh:5949c4a3d4a93f841f155cdb7e991c087e637145c1630572e21948224f8f4923",
|
||||
"zh:76d60f366b743003c1b085afa769b45b2198ee919927e45807d7d44fb42c067d",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:79cd1bab1261a07f84e917191d7ddc4340ac5f5524283767256f7ffd7f87caf0",
|
||||
"zh:8ec9083038cf710b30e319eaa467c9df7fa52bbd9969b61053a35bc2cdd2e0a6",
|
||||
"zh:a6e502cb579685ab7aeb886c2bb11ddd9cfed74b41008592d57cbc3351a9218b",
|
||||
"zh:acb74d6b4f66ff6acfcda315df802a7432170ef3955c9b432cb4580767004006",
|
||||
"zh:f0ce55d8d9ffdb33dab612b1246f9bab060a9d54fc32ce2b4a038646155660af",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
locals {
|
||||
templated_code = templatefile("${path.module}/scripts/Taupi-4.0.js", {
|
||||
sensor_aussen_mac = var.sensor_aussen_mac
|
||||
sensor_innen_mac = var.sensor_innen_mac
|
||||
taupunktschwelle = var.taupunktschwelle
|
||||
mindesttemperatur = var.mindesttemperatur
|
||||
mindesthumi = var.mindesthumi
|
||||
schaltzeit = var.schaltzeit
|
||||
battery_warngrenze = var.battery_warngrenze
|
||||
lost_connection = var.lost_connection
|
||||
})
|
||||
}
|
||||
|
||||
resource "local_file" "templated_script" {
|
||||
content = local.templated_code
|
||||
filename = "${path.module}/scripts/Taupi-4.0.templated.js"
|
||||
}
|
||||
|
||||
resource "shelly_script_config" "taupi" {
|
||||
ip = var.shelly_ip
|
||||
id = 1
|
||||
name = "Taupi-4.0"
|
||||
enable = true
|
||||
}
|
||||
|
||||
resource "null_resource" "upload_script" {
|
||||
triggers = {
|
||||
code_hash = sha256(local_file.templated_script.content)
|
||||
ip = var.shelly_ip
|
||||
}
|
||||
|
||||
provisioner "local-exec" {
|
||||
command = "python3 ${path.module}/scripts/upload.py ${var.shelly_ip} 1 ${local_file.templated_script.filename}"
|
||||
}
|
||||
|
||||
depends_on = [shelly_script_config.taupi]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
shelly = {
|
||||
source = "DonRobo/shelly"
|
||||
version = "~> 0.4.1"
|
||||
}
|
||||
null = {
|
||||
source = "hashicorp/null"
|
||||
version = "~> 3.2"
|
||||
}
|
||||
local = {
|
||||
source = "hashicorp/local"
|
||||
version = "~> 2.4"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.0"
|
||||
}
|
||||
|
||||
provider "shelly" {}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
////////////// TAUPI 4.0 @ Shelly //////////////
|
||||
// copyright by boeserbob und holzachr
|
||||
// Fragen an quirb@web.de
|
||||
// Dokumentation und aktuelle Versionen unter https://github.com/BoeserBob/Taupi-4.0
|
||||
//
|
||||
// Dieses Skript verwandelt z.B. eine Shelly Plug in eine Taupunktlüftersteuerung.
|
||||
// Der Skript schaltet einen angeschlossenen Lüfter über den Schalter des Shellys auf dem er installiert ist entsprechend der Taupunktunterschiede innen - außen.
|
||||
// - Es empfängt Messwert-Events von BLE-Sensoren auf.
|
||||
// - Wenn die Messwerte von den angegebenen Innen- und Außen-Sensoren stammen, werden aus Temperatur und Luftfeuchte die jeweiligen Taupunkte berechnet.
|
||||
// - Eine Timerschleife überprüft regelmäßig, ob alle Einschaltbedingungen fuer den Lüfter erfüllt sind:
|
||||
// - Wenn der Taupunkt innen größer als der Taupunkt außen + einem Schwellwert ist wird der Lüfter eingeschaltet, sonst ausgeschaltet.
|
||||
// - Wenn die Innentermperatur unter 10 °C und die Innenraumfeuchte unter 50 % ist wird der Lüfer ausgeschaltet.
|
||||
|
||||
//========== Sensor-Konfiguration ==========
|
||||
var sensor_aussen = "${sensor_aussen_mac}";
|
||||
var sensor_innen = "${sensor_innen_mac}";
|
||||
//========== Debug-Konfiguration ==========
|
||||
var DEBUG = false;
|
||||
//========== Schalt-Konfiguration ==========
|
||||
var taupunktschwelle = ${taupunktschwelle}; // [°C] Lüfter einschalten wenn TPinnen > (TPaussen + taupunktschwelle)...
|
||||
var mindesttemperatur = ${mindesttemperatur}; // [°C] ...und Tinnen > mindesttemperatur...
|
||||
var mindesthumi = ${mindesthumi}; // [%] ...und RHinnen > mindesthumi
|
||||
var schaltzeit = ${schaltzeit}; // [s] Schaltbedingung prüfen alle X Sekunden
|
||||
var battery_warngrenze = ${battery_warngrenze}; // [%] wenn dieser Schwellwert unterschritten ist blinkt der Plug rot
|
||||
var lost_connection = ${lost_connection}; // [s] Zeit nach der frische Sensordaten gekommen sein müssen um tote Verbindungen zu finden
|
||||
//===== Ende Sensor-Konfiguration === AB HIER MUSS NICHTS MEHR GEÄNDERT WERDEN =====================================
|
||||
|
||||
var taupunkt_aussen;
|
||||
var taupunkt_innen;
|
||||
var temperatur_innen;
|
||||
var temperatur_aussen;
|
||||
var humidity_innen;
|
||||
var humidity_aussen;
|
||||
var battery_innen;
|
||||
var battery_aussen;
|
||||
var lost_connection_innen;
|
||||
var lost_connection_aussen;
|
||||
|
||||
var luefterstatus = null; // Merkt sich letzten Schaltzustand, um unnötige Schaltvorgänge zu vermeiden
|
||||
|
||||
// Taupunktberechnung
|
||||
function taupunkt(T, RH) {
|
||||
var a = (T >= 0) ? 17.27 : 21.875;
|
||||
var b = (T >= 0) ? 237.7 : 265.5;
|
||||
var alpha = (a * T) / (b + T) + Math.log(RH / 100);
|
||||
return (b * alpha) / (a - alpha);
|
||||
}
|
||||
|
||||
// Lüftersteuerung
|
||||
function schalten() {
|
||||
// Sicherheitsprüfung: Sind alle benötigten Werte vorhanden?
|
||||
if (typeof taupunkt_innen === "undefined" ||
|
||||
typeof taupunkt_aussen === "undefined" ||
|
||||
typeof temperatur_innen === "undefined" ||
|
||||
typeof humidity_innen === "undefined")
|
||||
{
|
||||
print("Nicht alle Sensorwerte vorhanden - Schaltung übersprungen.");
|
||||
farbring(80,80,0,100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sicherheitsprüfung kommen regelmäßig frische Daten von den Sensoren?
|
||||
lost_connection_innen = lost_connection_innen + schaltzeit
|
||||
lost_connection_aussen = lost_connection_aussen + schaltzeit
|
||||
print("letzte Verbindung zum Sensor innen vor " ,lost_connection_innen, " Sekunden " );
|
||||
print("letzte Verbindung zum Sensor außen vor " ,lost_connection_aussen, " Sekunden " );
|
||||
|
||||
if (lost_connection_innen > lost_connection ||
|
||||
lost_connection_aussen > lost_connection )
|
||||
{
|
||||
print("Verbindung zu Sensoren zu lange verloren, Lüfter ausschalten.");
|
||||
Shelly.call("Switch.Set", { id: 0, on: false });
|
||||
farbring(80,80,0,100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Visualisierung Batteriefüllstand durch roten Blink
|
||||
if (battery_innen < battery_warngrenze ||
|
||||
battery_aussen < battery_warngrenze)
|
||||
{
|
||||
print("Batteriestand niedrig");
|
||||
farbring(100,0,0,100);
|
||||
}
|
||||
|
||||
// Schaltlogik (immer schalten, der Shelly schaltet nur, wenn er schalten muss).
|
||||
if ( temperatur_innen > mindesttemperatur &&
|
||||
humidity_innen > mindesthumi &&
|
||||
taupunkt_innen > taupunkt_aussen + taupunktschwelle
|
||||
)
|
||||
{
|
||||
print("Lüfter einschalten");
|
||||
Shelly.call("Switch.Set", { id: 0, on: true });
|
||||
farbring(80,10,0,100);
|
||||
|
||||
} else {
|
||||
print("Lüfter ausschalten.");
|
||||
Shelly.call("Switch.Set", { id: 0, on: false });
|
||||
farbring(0,0,80,100);
|
||||
}
|
||||
}
|
||||
|
||||
// Farbe Farbring setzen für Standalone Betrieb.
|
||||
function farbring(red,green,blue,helligkeit) {
|
||||
Shelly.call(
|
||||
"PLUGS_UI.SetConfig",{ id:0, config:{"leds":{"mode":"switch","colors":
|
||||
{"switch:0":
|
||||
{"on":{"rgb":[red,green,blue],"brightness":helligkeit},
|
||||
"off":{"rgb":[red,green,blue],"brightness":helligkeit}}}}}},
|
||||
function (result, code, msg, ud) {
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
// Event-Verarbeitung
|
||||
function checkBlu(event) {
|
||||
var addr = event.address.toLowerCase();
|
||||
var aussen = sensor_aussen.toLowerCase();
|
||||
var innen = sensor_innen.toLowerCase();
|
||||
|
||||
if (DEBUG) {
|
||||
print("DEBUG checkBlu: event.addr=", addr, " aussen=", aussen, " innen=", innen);
|
||||
}
|
||||
|
||||
if (addr === aussen) {
|
||||
temperatur_aussen = event.temperature;
|
||||
humidity_aussen = event.humidity;
|
||||
taupunkt_aussen = taupunkt(event.temperature, event.humidity);
|
||||
battery_aussen = event.battery;
|
||||
lost_connection_aussen = 0;
|
||||
print("Neue Werte für Außen:", temperatur_aussen, "°C,", humidity_aussen, "%, Tp:", taupunkt_aussen, "°C, Batt: ", battery_aussen, " % ");
|
||||
} else if (addr === innen) {
|
||||
temperatur_innen = event.temperature;
|
||||
humidity_innen = event.humidity;
|
||||
taupunkt_innen = taupunkt(event.temperature, event.humidity);
|
||||
battery_innen = event.battery;
|
||||
lost_connection_innen = 0;
|
||||
print("Neue Werte für Innen:", temperatur_innen, "°C,", humidity_innen, "%, Tp:", taupunkt_innen, "°C, Batt: " , battery_innen, " % ");
|
||||
}
|
||||
}
|
||||
|
||||
// Haupt-Timer für Steuerlogik
|
||||
Timer.set(schaltzeit * 1000, true, function () {
|
||||
print("----- Steuerung alle", schaltzeit, "s -----");
|
||||
print("Innen: T =", temperatur_innen, "°C, RH =", humidity_innen, "%, Tp =", taupunkt_innen, "Batterie: ", battery_innen, " % ");
|
||||
print("Außen: T =", temperatur_aussen, "°C, RH =", humidity_aussen, "%, Tp =", taupunkt_aussen, "Batterie: ", battery_aussen, " % ");
|
||||
schalten();
|
||||
});
|
||||
|
||||
///////////////// BLE-Decoder ///////////////////////
|
||||
const BTHOME_SVC_ID_STR = "fcd2";
|
||||
|
||||
const uint8 = 0;
|
||||
const int8 = 1;
|
||||
const uint16 = 2;
|
||||
const int16 = 3;
|
||||
const uint24 = 4;
|
||||
const int24 = 5;
|
||||
const dimmert = 6; // special data type for dimmer event
|
||||
const uint32 = 7;
|
||||
const int32 = 8;
|
||||
|
||||
// The BTH object defines the structure of the BTHome data
|
||||
const BTH = {
|
||||
0x00: { n: "pid", t: uint8 },
|
||||
0x01: { n: "battery", t: uint8, u: "%" },
|
||||
0x02: { n: "temperature", t: int16, f: 0.01, u: "tC" },
|
||||
0x04: { n: "atm. pressure", t: int24, f: 0.01, u: "hPa" },
|
||||
0x03: { n: "humidity", t: uint16, f: 0.01, u: "%" },
|
||||
0x05: { n: "illuminance", t: uint24, f: 0.01 },
|
||||
0x08: { n: "dew point", t: uint16, f: 0.01, u: "tC" },
|
||||
0x0C: { n: "capacitor voltage", t: uint16, f: 0.001, u: "V" },
|
||||
0x1e: { n: "light", t: uint8 },
|
||||
0x20: { n: "raining", t: uint8},
|
||||
0x21: { n: "motion", t: uint8 },
|
||||
0x2d: { n: "window", t: uint8 },
|
||||
0x2e: { n: "humidity", t: uint8, u: "%" },
|
||||
0x3a: { n: "button", t: uint8 },
|
||||
0x3c: { n: "dimmer", t: dimmert },
|
||||
0x3f: { n: "rotation", t: int16, f: 0.1 },
|
||||
0x44: { n: "wind speed", t: int16, f: 0.01, u: "m/s" },
|
||||
0x45: { n: "temperature", t: int16, f: 0.1, u: "tC" },
|
||||
0x46: { n: "UV index", t: int8, f: 0.1 },
|
||||
0x5E: { n: "wind direction", t: int16, f: 0.01 },
|
||||
0x5F: { n: "precipitation", t: int16, f: 0.1, u: "mm" },
|
||||
0x60: { n: "channel", t: uint8 },
|
||||
0xf0: { n: "dev_type", t: uint16 },
|
||||
0xf1: { n: "fw_version", t: uint32 },
|
||||
0xf2: { n: "fw_version_short", t: uint24 },
|
||||
};
|
||||
|
||||
function getByteSize(type) {
|
||||
if (type === uint8 || type === int8) return 1;
|
||||
if (type === uint16 || type === int16 || type === dimmert) return 2;
|
||||
if (type === uint24 || type === int24) return 3;
|
||||
if (type === uint32 || type === int32) return 4;
|
||||
return 255;
|
||||
}
|
||||
|
||||
// Functions for decoding and unpacking the service data from Shelly BLU devices
|
||||
const BTHomeDecoder = {
|
||||
utoi: function (num, bitsz) {
|
||||
const mask = 1 << (bitsz - 1);
|
||||
return num & mask ? num - (1 << bitsz) : num;
|
||||
},
|
||||
getUInt8: function (buffer) {
|
||||
return buffer.at(0);
|
||||
},
|
||||
getDimmer: function (buffer) {
|
||||
return {"dimmer": buffer.at(0), "dimmersteps": buffer.at(1)};
|
||||
},
|
||||
getInt8: function (buffer) {
|
||||
return this.utoi(this.getUInt8(buffer), 8);
|
||||
},
|
||||
getUInt16LE: function (buffer) {
|
||||
return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
|
||||
},
|
||||
getInt16LE: function (buffer) {
|
||||
return this.utoi(this.getUInt16LE(buffer), 16);
|
||||
},
|
||||
getUInt24LE: function (buffer) {
|
||||
return (
|
||||
0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
|
||||
);
|
||||
},
|
||||
getInt24LE: function (buffer) {
|
||||
return this.utoi(this.getUInt24LE(buffer), 24);
|
||||
},
|
||||
getUInt32LE: function (buffer) {
|
||||
return 0xffffffff & ((buffer.at(3) << 24) | (buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0));
|
||||
},
|
||||
getInt32LE: function (buffer) {
|
||||
return this.utoi(this.getUInt32LE(buffer), 32);
|
||||
},
|
||||
getBufValue: function (type, buffer) {
|
||||
if (buffer.length < getByteSize(type)) return null;
|
||||
let res = null;
|
||||
if (type === uint8) res = this.getUInt8(buffer);
|
||||
if (type === int8) res = this.getInt8(buffer);
|
||||
if (type === uint16) res = this.getUInt16LE(buffer);
|
||||
if (type === int16) res = this.getInt16LE(buffer);
|
||||
if (type === uint24) res = this.getUInt24LE(buffer);
|
||||
if (type === int24) res = this.getInt24LE(buffer);
|
||||
if (type === uint32) res = this.getUInt32LE(buffer);
|
||||
if (type === int32) res = this.getInt32LE(buffer);
|
||||
if (type === dimmert) res = this.getDimmer(buffer);
|
||||
return res;
|
||||
},
|
||||
|
||||
unpack: function (buffer) {
|
||||
if (typeof buffer !== "string" || buffer.length === 0) return null;
|
||||
let result = {};
|
||||
let _dib = buffer.at(0);
|
||||
result["encryption"] = _dib & 0x1 ? true : false;
|
||||
result["BTHome_version"] = _dib >> 5;
|
||||
if (result["BTHome_version"] !== 2) return null;
|
||||
if (result["encryption"]) return result;
|
||||
buffer = buffer.slice(1);
|
||||
|
||||
let _bth;
|
||||
let _value;
|
||||
while (buffer.length > 0) {
|
||||
_bth = BTH[buffer.at(0)];
|
||||
if (typeof _bth === "undefined") {
|
||||
print("BTH: Unknown type: ", buffer.at(0));
|
||||
break;
|
||||
}
|
||||
buffer = buffer.slice(1);
|
||||
_value = this.getBufValue(_bth.t, buffer);
|
||||
|
||||
// handle dimmer special case
|
||||
if (typeof _value === "object" && _bth.t === dimmert) {
|
||||
result["dimmersteps"] = _value["dimmersteps"];
|
||||
_value = _value["dimmer"];
|
||||
}
|
||||
|
||||
if (_value === null) break;
|
||||
if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
|
||||
|
||||
if (typeof result[_bth.n] === "undefined") {
|
||||
result[_bth.n] = _value;
|
||||
}
|
||||
else {
|
||||
if (Array.isArray(result[_bth.n])) {
|
||||
result[_bth.n].push(_value);
|
||||
}
|
||||
else {
|
||||
result[_bth.n] = [
|
||||
result[_bth.n],
|
||||
_value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
buffer = buffer.slice(getByteSize(_bth.t));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
let lastPacketId = 0x100;
|
||||
|
||||
function BLEScanCallback(event, result) {
|
||||
if (event !== BLE.Scanner.SCAN_RESULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
print("DEBUG BLEScanCallback: addr=", result.addr, " rssi=", result.rssi);
|
||||
}
|
||||
|
||||
if (typeof result.service_data === "undefined" ||
|
||||
typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
let unpackedData = BTHomeDecoder.unpack(result.service_data[BTHOME_SVC_ID_STR]);
|
||||
|
||||
if (unpackedData === null ||
|
||||
typeof unpackedData === "undefined" ||
|
||||
unpackedData["encryption"]) {
|
||||
print("Error: Encrypted devices are not supported or unpacking failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
print("DEBUG unpacked BTHome:", JSON.stringify(unpackedData));
|
||||
}
|
||||
|
||||
if (lastPacketId === unpackedData.pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPacketId = unpackedData.pid;
|
||||
unpackedData.address = result.addr;
|
||||
checkBlu(unpackedData);
|
||||
}
|
||||
|
||||
function initBLE() {
|
||||
const BLEConfig = Shelly.getComponentConfig("ble");
|
||||
|
||||
if (!BLEConfig.enable) {
|
||||
print("Error: The Bluetooth is not enabled, please enable it from settings");
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLE.Scanner.isRunning()) {
|
||||
print("Info: The BLE gateway is running, the BLE scan configuration is managed by the device");
|
||||
}
|
||||
else {
|
||||
const bleScanner = BLE.Scanner.Start({
|
||||
duration_ms: BLE.Scanner.INFINITE_SCAN,
|
||||
active: false
|
||||
});
|
||||
|
||||
if(!bleScanner) {
|
||||
print("Error: Can not start new scanner");
|
||||
}
|
||||
}
|
||||
|
||||
BLE.Scanner.Subscribe(BLEScanCallback);
|
||||
}
|
||||
|
||||
initBLE();
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import sys
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
def upload(ip, script_id, code_file):
|
||||
# Read JS code
|
||||
with open(code_file, 'r', encoding='utf-8') as f:
|
||||
code = f.read()
|
||||
|
||||
url = f"http://{ip}/rpc"
|
||||
|
||||
# Stop the script first to prevent write locks
|
||||
print(f"Stopping script {script_id}...")
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps({"id": 1, "method": "Script.Stop", "params": {"id": int(script_id)}}).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(req)
|
||||
except Exception as e:
|
||||
print(f"Stop request failed (might already be stopped): {e}")
|
||||
|
||||
# Chunk the code into 500 byte chunks (Shelly RPC size limit)
|
||||
chunk_size = 500
|
||||
chunks = [code[i:i + chunk_size] for i in range(0, len(code), chunk_size)]
|
||||
|
||||
for idx, chunk in enumerate(chunks):
|
||||
append = idx > 0
|
||||
print(f"Uploading chunk {idx + 1}/{len(chunks)} (append={append})...")
|
||||
payload = {
|
||||
"id": 1,
|
||||
"method": "Script.PutCode",
|
||||
"params": {
|
||||
"id": int(script_id),
|
||||
"code": chunk,
|
||||
"append": append
|
||||
}
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode('utf-8'),
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
res = json.loads(resp.read().decode('utf-8'))
|
||||
if "error" in res:
|
||||
raise Exception(f"Failed to upload chunk: {res['error']}")
|
||||
|
||||
print("Script upload complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 4:
|
||||
print("Usage: python3 upload.py <ip> <script_id> <code_file>")
|
||||
sys.exit(1)
|
||||
upload(sys.argv[1], sys.argv[2], sys.argv[3])
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
sensor_aussen_mac = "C0:2C:ED:21:4A:2B"
|
||||
sensor_innen_mac = "FC:4D:6A:12:D9:5F"
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
variable "shelly_ip" {
|
||||
type = string
|
||||
description = "The local IP address of the Shelly Plug S (Gen3) controller"
|
||||
default = "192.168.10.168"
|
||||
}
|
||||
|
||||
variable "sensor_aussen_mac" {
|
||||
type = string
|
||||
description = "The Bluetooth MAC address of the outdoor sensor (e.g. Shelly BLU H&T)"
|
||||
}
|
||||
|
||||
variable "sensor_innen_mac" {
|
||||
type = string
|
||||
description = "The Bluetooth MAC address of the indoor sensor (e.g. Shelly BLU H&T)"
|
||||
}
|
||||
|
||||
variable "taupunktschwelle" {
|
||||
type = number
|
||||
description = "Dew point difference threshold [°C] to trigger the fan (indoor_dp > outdoor_dp + threshold)"
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "mindesttemperatur" {
|
||||
type = number
|
||||
description = "Minimum indoor temperature [°C] required to run the fan"
|
||||
default = 10
|
||||
}
|
||||
|
||||
variable "mindesthumi" {
|
||||
type = number
|
||||
description = "Minimum indoor relative humidity [%] required to run the fan"
|
||||
default = 50
|
||||
}
|
||||
|
||||
variable "schaltzeit" {
|
||||
type = number
|
||||
description = "Switch logic check interval in seconds"
|
||||
default = 180
|
||||
}
|
||||
|
||||
variable "battery_warngrenze" {
|
||||
type = number
|
||||
description = "Battery level warning threshold [%]"
|
||||
default = 20
|
||||
}
|
||||
|
||||
variable "lost_connection" {
|
||||
type = number
|
||||
description = "Time in seconds after which data is considered stale, causing the fan to turn off for safety"
|
||||
default = 600
|
||||
}
|
||||
Loading…
Reference in New Issue