Skip to content

Releases: Domktorymysli/vclu

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 16:33

Update

curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bash

It keeps your directories and web port and makes a backup first (docs). Docker: docker pull ghcr.io/domktorymysli/vclu:latest.

Import the OM project again to get the new objects: Smart Panel panels and buttons, a CLU's thermostats (RemoteThermostat) and the channels of an LED RGBW module. New objects are not subscribed until you add them on Grenton → Sync and press Sync now.

Home Assistant: a Grenton RGB light that HA already had as a plain light is removed and announced again once as a colour light; a name or area set for it in HA has to be set again.

Contract change: execute() and set() on a remote object send a string argument as a quoted Lua string. A module that put quotes around a string itself (obj:execute(3, '"' .. text .. '"')) must drop them.

Checked on hardware: Smart Panel WiFi (panel module fv05), LED RGBW DIN fv01 (colour and brightness). Not yet: a thermostat on a panel, the white and the channels of an LED RGBW module, a Smart Panel on a CLU's bus.

Grenton

  • The channels of an LED RGBW module are lights - the OM import did not know LED_CHANNEL (class 49, four per LED RGBW 2.0 module), so a strip driven per channel had nothing in vCLU. Each channel is now a RemoteLedChannel: a dimmer on vCLU's 0..1 scale (setBrightness, switchOn, switchOff, toggle, dim), converted from and to the module's 0..255; Home Assistant gets it as a dimmable light, exposeRegistry() as a dimmer (HomeKit too). Import the OM project again to get them (#41). Not tested on Grenton LED hardware.
  • A diagnostic pack import names the CLU that failed - an error showed the path inside the zip (CLU511000000/om.lua: could not extract serial number from CLU ID), while the imported CLUs are listed by name. Errors and the skipped vCLU now read Alarm (CLU511000000): …, with the name taken from the Object Manager project in the pack (the bare ID when the pack has none). A CLU whose om.lua Object Manager left empty, as it does for a CLU it could not read, is reported as empty instead of as a bad serial number (#58).
  • vCLU can run a Grenton Smart Panel - the OM import now creates the panel (RemotePanel, class 29) and its buttons (RemotePanelButton, class 30, attached to the panel by index). In Object Manager every panel event calls click("<panel name>", "btn1") (also btn1:long, btn1:hold, swipe:up, proximity, wake), and a module describes the screens with require("lib/panel"): a menu that edits values in place (:toggle, :number with min/max, :choice, :action with confirm), pages for a value, an on/off object, a thermostat and an hourly bar chart of a value's history (Panel.chart, opened from a menu value row), the last changes of an object with their times (Panel.history), a page where each corner is one button (Panel.corners, e.g. home with a room's lights and a Menu corner), an idle screen with time and values, a cleaning lock with a countdown, panel:notify(), and page types of your own (Panel.register). The four corner buttons mean the same on every page: up and down on the left, back top right (<) and OK bottom right (>); their LEDs show which ones work. A menu leaves out a submenu with no rows, and a row given a registry path (:toggle("Swiatlo", "CLU221000290.DOU4157")) is left out when the object is missing, so a module can list rooms whose objects may be missing. Helpers for a module: Panel.new("PAN1085") takes the panel from the import by id and restores click() (Object Manager's user.lua hides it with an empty one), Panel.menu(name, { submenus }), :dimmer(label, obj), a format table ({ [0] = "closed", [1] = "open" }), and Panel.chart/Panel.history give no page for an object without history. Ready widgets: menu:add(label, obj) picks the row by what the device is (its expose() type, else its Grenton type): a switch or light switches, a dimmer edits its brightness, a cover opens a page where up opens, down closes and OK stops, a temperature opens its chart, a binary sensor its history; menu:gate(label, obj, { open = 0 }) shows a gate open or closed with its history; and Panel.area("Salon") is a menu of what expose() put in that area, named as in Home Assistant and HomeKit (lights, dimmers, covers, temperature and humidity; switches such as pumps or door openers only when asked for). Panel.floor("Parter") lists the areas exposed with floor = "Parter", in an order and with titles you give. As with homekit and mqtt, expose(..., { panel = false }) keeps an object off the panels and panel = true puts it on whatever its kind. Corners take a path and act by device (a cover opens or closes), and clock = true draws the time. A CLU's thermostats (class 18) are imported as RemoteThermostat (temperature at its sensor as the value, getTarget(), getSetpoint(), setSetpoint(), state and mode), in the registry for the Sync list but not exposed to Home Assistant or HomeKit (an object with no expose type is now left out of the automatic exposure instead of becoming a switch; expose() without a type refuses it); menu:thermostat(label, obj, { readonly = true }) shows one on a panel, and without readonly + and - write its set point in manual mode (in holiday or auto mode the page only shows). Thermostats on CLU firmware 197 and 1f5 (Thermostat v1) have no current temperature. A value set from a panel stays on the screen until the device reports it (or reports anything else, e.g. a clamped value; at most Panel.HOLD_S, 90 s), so a device that reads back the old value for a while (a recuperator over HTTP) does not look unchanged. Long labels are cut before their value. The texts the library draws itself are English and set in Panel.strings; Panel.lang("pl") switches them to Polish. Frames go out in short spaced messages, each one expression (a Smart Panel runs nothing of a message with two statements), and a frame without an answer is sent again whole. Import the OM file again after updating. Checked on a Smart Panel WiFi (panel module fv05): menu, edits, idle screen and button events; DrawBox draws nothing there, so the library never fills boxes, and OnClick does not fire after a long press, so OnClick and OnLongPress are the events to wire (#44).
  • Text sent to a CLU is quoted - execute() on a remote object sent a string argument without quotes, so the CLU got a bare name (nil); set() did not escape quotes or newlines. Both now send a Lua string literal. RemoteRGBLight:setRGB quoted the colour itself and now passes it plain.

Installation

  • The Docker image runs vCLU as a user, not as root, and reports its health - ghcr.io/domktorymysli/vclu starts vCLU as vclu (UID/GID 1000). An entrypoint runs as root only to hand the data directory to that user (a volume written by an earlier, root image, or a host directory mounted as /data, which gets owner 1000:1000) and to give it access to a passed-in /dev/gpiomem, then drops root with su-exec; a step it cannot do is logged and does not stop the start. The binary has cap_net_bind_service, so TFTP on port 69 opens without --cap-add (not with --security-opt no-new-privileges). HEALTHCHECK calls /api/health on the --web-port vCLU was started with. A named volume needs nothing. Check: a container run with --cap-drop ALL (or Kubernetes drop: [ALL]) needs --cap-add NET_BIND_SERVICE, or vCLU does not start; one run with --user (not root) runs vCLU as that user, whose data directory must be writable by it. docker exec still enters as root, docker exec -u vclu as vCLU's user (#3).

AI agents (MCP)

  • Agents can write and test a Smart Panel module - vclu_draft_test takes {"click": "PAN1085", "input": "btn3"} steps, the input a CLU sends when a panel button is pressed, and the frames the panel would get show in clu_calls after an advance; a click no panel takes fails the step. vclu_runtime_schema describes lib/panel (pages, screen, Object Manager wiring), docs/ai/examples.md has a panel module, and the generated docs/ai/lua-api-reference.md lists lib/panel and RemotePanel. The reference now names the arguments of functions documented without @param (e.g. Dashboard.new(id)) (#44).
  • Agents are told to put code in modules - the MCP instructions and docs/ai/examples.md now say: read modules/init.lua and follow its layout; a domain (a plugin, a device, a room) is modules/<domain>/init.lua with its readings, metrics and exports, and its dashboard in modules/dashboards/ reads from that module, not from the plugin directly, so the dashboard and every export show the same values. A flat modules/*.lua is for a short standalone script, and must not sit next to a directory of the same name, which require would then never reach. Before this, an agent asked for a dashboard tended to drop a loose file in modules/.
  • vclu_draft_promote no longer warns about a module's init.lua that is required by its directory name - promoting modules/pogoda/init.lua warned that modules/init.lua has no require('pogoda/init'), though require("pogoda") loads it.

Plugins

  • A factory plugin is no longer marked "not configured" - the plugins page showed the badge on every installed plugin without saved configuration, including factory plugins such as MusicCast, which declare none and are created from code. The badge now shows only for a plugin that asks for configuration and has none, which is also when the loader skips it (`...
Read more

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 20:36

Update

curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bash

It keeps your directories and web port and makes a backup first (docs). Docker: docker pull ghcr.io/domktorymysli/vclu:latest.

Contract change, cover position: vCLU now counts a cover's position 0 = closed .. 100 = open, as Grenton, Home Assistant and HomeKit do. For VirtualCover and RemoteCover nothing changes in Home Assistant or HomeKit. A Lua module that reads or sets a position (getPosition, setPosition, onChange) needs 100 - p. Details below, under Lua runtime.

Grenton rollers (ROLLER_SHUTTER): if you have not done it since v1.4.0, import the Object Manager project again (or POST /api/om/regenerate) so the roller gets its position.

None of the roller, dimmer and LED changes have been tested on Grenton hardware yet: a report is welcome.

Lua runtime

  • A cover's position counts 0 = closed .. 100 = open, as in Grenton, Home Assistant and HomeKit - vCLU counted the other way (0 = open), so a blind at 30 % on the dashboard was at 70 % in the Grenton app, and every integration converted. VirtualCover and RemoteCover now use Grenton's scale: getPosition(), setPosition(), the value, onChange, history and the dashboard slider; open() sets 100, close() 0. The conversions in Home Assistant and HomeKit are gone, so for these two classes nothing changes there. A dashboard cover whose roller has not reported a position shows -- instead of 0. Contract change: a module that reads or sets a position must swap it (100 - p); an own object exposed as cover must count 0 = closed; a VirtualCover without initial now starts closed (0); a VirtualCover kept with persist and positions in history or exported metrics from before the update are on the old scale. Not tested on Grenton roller hardware.

Grenton

  • An RGB(W) LED switches on and off, and dims - RemoteRGBLight (LEDRGB, class 9) inherited the dimmer's method numbers, which mean something else on a LED: switchOn() sent SetSaturation (the colour turned white), switchOff() SetRedValue (the red channel went dark), toggle() SetHue, dim() SetValue, and setValue/setBrightness wrote the read-only Value, so nothing changed. They now send SwitchOn 9, SwitchOff 10, Switch 11, HoldValue 7 and SetValue (execute 0, with an optional ramp), as listed in Object Manager's device definitions. New getWhite()/setWhite(0..255, ramp) for the white channel of an RGBW module. Not tested on Grenton LED hardware.
  • A dimmer's setValue with a ramp sets the value - setValue(v, ramp) sent execute 0, which on a dimmer is Hold (dimming while a button is held), not SetValue. A dimmer has no SetValue with a ramp, so the ramp is written to its RampTime (feature 1) and the value with set(0); the RampTime stays for later changes. Not tested on Grenton dimmer hardware.
  • Brightness is rounded, not cut - getBrightness() and the brightness sent to Home Assistant turned 0.29 into 28 %. getBrightness() returns nil until the dimmer reports, instead of an error.

Home Assistant

  • A cover published with expose() moves, and runs the right way - a position from Home Assistant was written to the object's feature 0, which on a Grenton roller is its read-only state, so the slider moved nothing; OPEN and CLOSE were sent the same way, and the position went to HA unconverted (vCLU counts 0 = open, HA 0 = closed). Commands now call the object's own setPosition, open, close and stop (setValue only for objects without them), positions are converted both ways, discovery announces position_open: 100 / position_closed: 0, and the state is open whenever the cover is not fully closed. A roller without a position (class 11, or a proxy not regenerated) gets no position slider in HA, a position sent to it is refused (it would call a method class 11 does not have), a position retained by older versions is cleared. A Grenton roller's movement is shown as opening/closing while it moves (also when only the movement changed, e.g. a stop at the same position), and a roller without a position reports stopped when it stops. Contract change: the position on vclu/exposed/.../cover/.../position is now on HA's scale; a Home Assistant automation that compensated for the inverted position needs changing (#36, thanks to Dawkaxxl for the logs). Not tested on Grenton roller hardware.
  • open() and close() of a Grenton roller with a position go to SetPosition - they sent MoveUp/MoveDown, which stop a roller that is moving (Object Manager's device definitions), so a second OPEN from Home Assistant, HomeKit or Lua stopped the roller halfway. With a position and no time they now send SetPosition 100 % / 0 %; with a time (open(5000)) or without a position MoveUp/MoveDown stay. Not tested on Grenton roller hardware.
  • HomeKit opens and closes a Grenton roller without a position - the Home app's slider and tap sent SetPosition, which a roller without a position (class 11) does not have, so it did not move. Such a roller now opens at a target of 50 % or more and closes below. A roller from a proxy generated before v1.4.0 counts as one without a position too: import the OM project again to get its position back. Not tested on Grenton roller hardware.
  • An RGB LED from the OM import can be controlled - it is announced with schema: json, so Home Assistant sends one JSON command ({"state":"ON","brightness":40,"color":{...}}), which was refused as an unknown payload. The command is now read: OFF, brightness, an rgb or hs colour, ON. The state no longer reports a made-up white colour. ON/OFF for lights call the object's switchOn/switchOff.

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 15 Oct 16:56

Update

curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bash

update.sh is new in this release: it keeps your directories and web port and makes a backup first (docs). Docker: docker pull ghcr.io/domktorymysli/vclu:latest.

Grenton rollers (ROLLER_SHUTTER): after updating, import the Object Manager project again (or POST /api/om/regenerate) so Home Assistant gets the roller's position.

Installation

  • deploy/update.sh updates an installation - running the installer again reset the web port to 8080 and rewrote the service without a backup. The updater reads the install and data directories and the port from the systemd service, does nothing when that version is installed (--force to reinstall, --version to pick one), stops vCLU and packs the program, web files, runtime, service file and the configuration from the data directory (Lua, plugins, KV, HomeKit pairing; not history, metrics or backups) into <install dir>/backup-<version>-<time>.tar.gz, readable by root only (the three newest are kept, pruned only after a good update), starts vCLU again if packing fails, and prints how to restore it if the install fails. A stopped service with the target version installed is installed again rather than reported up to date. Without curl, tar or gzip it stops before changing anything and says how to install them. It stops when the service file was edited, since the installer rewrites it (--force to go on; keep changes in systemctl edit vclu). Data is not touched except the bundled AI docs, as by the installer.

Dashboards

  • A status tile's colour can follow its value - color on :status() (section or header) can be a function, e.g. function() if smsLeft < 20 then return 'danger' end end. It is read on every poll: an open page recolours the tile (in the header, its icon) without a reload and drops the colour when the function returns nil. An error in the function leaves the value shown, drops the colour and is logged once per tile. A string color works as before (#8).

Lua runtime

  • A VirtualNumber with a fractional step stores the value on the step - snapping to step = 0.1 left binary noise, so 53.9 was stored and published as 53.900000000000006 to Home Assistant, HomeKit, MQTT and history. The snapped value is rounded to the step's decimals (#9, thanks @wzd8888).
  • EventBus warns when callbacks for one event pile up - code that calls EventBus:on in a timer or on every re-create without off adds callbacks for ever and every emit runs them all. At 100 callbacks for one event name on logs a warning with the name and the count, and again at 200, 400...; the callbacks are still added (#7).
  • expose() warns about an object whose onChange returns no unsubscribe function - unexpose() calls that function to remove its callback; without it the callback stayed registered and kept running, silently. The log now shows one warning with the object's path. Plugin sensors and controls (plugin:sensor, plugin:control) now return one too, so unexpose() removes their callback (#5).

Plugins and HTTP

  • Plugins see the request error in resp.err - the plugin docs describe resp = { status, body, headers, json, err, errCode } and plugins check if resp.err then, but the runtime only passed the error as the callback's second argument, so resp.err was always nil: a refused connection reached plugins as a parse error and SolaX logged HTTP 0. resp.err now carries the same text as the second argument (vclu-plugins#3).

  • A poller warns when done() gets a string and has no onData - done("message") is data to the poller, so the tick counts as a success and onError never runs. The poller logs one warning pointing to done(nil, err); behaviour is unchanged.

  • Concurrent HTTP requests from Lua no longer lose each other's responses - plugins (PluginHelpers.httpRequest), HTTP clients (HTTP:async*) and Grenton HTTPRequest objects took completed requests from one shared queue and dropped the ones that were not theirs, so with several plugins polling at once some timed out at random or got HTTP 0. Each request now collects its own response. Two requests from one plugin in the same second no longer share an id (one of them used to get the other's response), an HTTP client reports the real transport error instead of HTTP 0, and responses nobody collects are dropped after 5 minutes (#19).

Grenton

  • A state change whose report was lost shows up within 30 s - a CLU reports a change with one UDP packet and no retry; when it was lost, vCLU kept the old state until another object in the same subscription changed (a light switched off from the myGrenton app stayed on in the dashboard). The CLU answers every subscription renewal (every 30 s) with the current values, and vCLU threw that answer away; it now applies it like a report (#29).
  • A roller reports its position - vCLU subscribed every object to feature 0, for a roller its movement (0 stopped, 1 up, 2 down), so Home Assistant showed 0-2 % and a stopped roller as closed, and HomeKit showed no position. A ROLLER_SHUTTER (class 24) is now subscribed to feature 7 Position as well; the position is the roller's value and its movement is read with getState(). Home Assistant gets the position with open/closed derived from it. Import the OM file again (or POST /api/om/regenerate) after updating: only a regenerated proxy marks a roller as having a position, an older one keeps the old behaviour. The first firmware of the 2.0 roller modules (fv01) has no position and om.lua does not tell it apart; set _hasPosition = false on such a roller.
  • A roller's position runs the right way - Grenton counts 0 % = closed .. 100 % = open (feature 7 and SetPosition, Object Manager's device definitions), vCLU 0 = open .. 100 = closed. RemoteCover passed the value unchanged, so setPosition(30) (30 % closed) left the roller 30 % open, from Lua, HomeKit and Home Assistant alike. It now converts both ways. Not tested on Grenton roller hardware.
  • Contract change: the value (_lastState, onChange, history) of a RemoteCover with a position is that position, 0 = open .. 100 = closed, not its movement; getState() still returns the movement and getPosition() the position. A roller without a position (class 11, or a proxy not regenerated) keeps its movement as the value and getPosition() returns nil.

Web

  • After updating a plugin, the plugins page no longer shows it as unconfigured - Install/Update stored the plugin under its short id while the page keys installed plugins by full id (@vclu/...), so the Installed tab showed a second entry with version ?, "not configured" and an empty config form until a reload. Install and update now answer with fullId and version; the page updates the entry under the full id, keeping its configuration and enabled state, and matches installed plugins to the catalog by full id, so the description and the update button appear on the Installed tab (#31).

Home Assistant

  • A dimmer published from the OM import follows the scale it announces - the registry light handler looked for the subpath brightness/set while the topic parser already strips the trailing /set, so a brightness command was refused as an "unknown payload" and the dimmer never moved. It also passed Home Assistant's 0..100 value straight to an object whose value is 0..1. A brightness command now uses the same path as expose(): 0..100 in, 0..1 out, clamped to the range, through setBrightness when the object has it (#1).
  • A roller published from the OM import moves to the position set in Home Assistant - the registry cover handler looked for the subpath position/set while the topic parser already strips the trailing /set, so a position command was refused as an "unknown payload". It also looked for SetPosition/Open/Close/Stop while a Grenton roller has them in lower case. A position now goes through setPosition, clamped to 0..100 and rounded, and OPEN/CLOSE/STOP through open/close/stop (#25).

MQTT

  • MQTT:destroy() releases a client, and MQTT:new(id) with an id in use no longer leaves the old one half alive - new overwrote the registry entry while the old client stayed connected and subscribed, and disconnect() kept its subscriptions and handlers, so a module or plugin that re-created its client leaked connections. destroy() unsubscribes on the broker, disconnects, clears subscriptions and event handlers and removes the client from Lua and Go; new(id) with an id in use destroys the old client first and logs a warning with the id. Messages and connection events still arriving from the old client are dropped instead of reaching the new one. The built-in client (global MQTT, used by the Home Assistant integration) cannot be destroyed or re-created. A Lua reload starts with an empty registry, so it does not trigger the warning (#4).

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 10 Oct 12:11

Changelog

  • Build: Go 1.26 and updated modules; HA: dimmers take 0..1; changelog
  • CHANGELOG: v1.3.0
  • CI: lint fix in MCP prompts; scan and sandbox tests stable under the race detector
  • CI: long sandbox scenario test gets more time under the race detector
  • Dashboard: PIN check; sandbox memory guard without false stops under load; tests independent of the internet and stable under the race detector
  • Dashboard: charts on public dashboards load without login; Satel: next scan not refused right after one finished
  • Docs: Polish README, links and Discord; installer runs with sudo; README and MCP instructions match promote behaviour
  • Docs: README audience line
  • Docs: agent example for Grenton objects without a proxy
  • HA: on/off lights without a brightness slider; Satel: names given in expose() stay; HomeKit: bridge picks up changed names
  • HomeKit: bridge starts with objects, stable accessory ids, no accidental switches, Satel read-only; MCP: mqtt_discover snippets that run, failed promote records an incident
  • HomeKit: lights switch with 1, dimmer brightness, covers in the right direction, async notifications, settings apply on save
  • License: limitation of liability, trademark note, copyright 2025-2026
  • Tests: check errors flagged by lint in the history and httpbin helpers

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 22:08

Changelog

  • Release: fix archive layout and multi-arch Docker images
  • Sandbox: memory guard, limits reach handlers and timers; safer promote and rollback
  • vCLU v1.2.0