From 85bfabd3872d8f7ebef3830f993d1b905ea5e478 Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Fri, 3 Jul 2026 09:07:08 +0200 Subject: [PATCH] feat(shelly): add terraform_shelly workspace for Taupi-4.0 dew point ventilation --- terraform_shelly/.gitignore | 13 + terraform_shelly/.terraform.lock.hcl | 66 +++++ terraform_shelly/main.tf | 37 +++ terraform_shelly/providers.tf | 19 ++ terraform_shelly/scripts/Taupi-4.0.js | 364 ++++++++++++++++++++++++++ terraform_shelly/scripts/upload.py | 56 ++++ terraform_shelly/terraform.tfvars | 2 + terraform_shelly/variables.tf | 51 ++++ 8 files changed, 608 insertions(+) create mode 100644 terraform_shelly/.gitignore create mode 100644 terraform_shelly/.terraform.lock.hcl create mode 100644 terraform_shelly/main.tf create mode 100644 terraform_shelly/providers.tf create mode 100644 terraform_shelly/scripts/Taupi-4.0.js create mode 100644 terraform_shelly/scripts/upload.py create mode 100644 terraform_shelly/terraform.tfvars create mode 100644 terraform_shelly/variables.tf diff --git a/terraform_shelly/.gitignore b/terraform_shelly/.gitignore new file mode 100644 index 0000000..6cd85c7 --- /dev/null +++ b/terraform_shelly/.gitignore @@ -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 diff --git a/terraform_shelly/.terraform.lock.hcl b/terraform_shelly/.terraform.lock.hcl new file mode 100644 index 0000000..2babc1f --- /dev/null +++ b/terraform_shelly/.terraform.lock.hcl @@ -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", + ] +} diff --git a/terraform_shelly/main.tf b/terraform_shelly/main.tf new file mode 100644 index 0000000..25ec756 --- /dev/null +++ b/terraform_shelly/main.tf @@ -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] +} diff --git a/terraform_shelly/providers.tf b/terraform_shelly/providers.tf new file mode 100644 index 0000000..ec7f6bc --- /dev/null +++ b/terraform_shelly/providers.tf @@ -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" {} diff --git a/terraform_shelly/scripts/Taupi-4.0.js b/terraform_shelly/scripts/Taupi-4.0.js new file mode 100644 index 0000000..24754a0 --- /dev/null +++ b/terraform_shelly/scripts/Taupi-4.0.js @@ -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(); diff --git a/terraform_shelly/scripts/upload.py b/terraform_shelly/scripts/upload.py new file mode 100644 index 0000000..6bb1f4f --- /dev/null +++ b/terraform_shelly/scripts/upload.py @@ -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 ") + sys.exit(1) + upload(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/terraform_shelly/terraform.tfvars b/terraform_shelly/terraform.tfvars new file mode 100644 index 0000000..610c03e --- /dev/null +++ b/terraform_shelly/terraform.tfvars @@ -0,0 +1,2 @@ +sensor_aussen_mac = "C0:2C:ED:21:4A:2B" +sensor_innen_mac = "FC:4D:6A:12:D9:5F" diff --git a/terraform_shelly/variables.tf b/terraform_shelly/variables.tf new file mode 100644 index 0000000..b18b4e6 --- /dev/null +++ b/terraform_shelly/variables.tf @@ -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 +}