Repository navigation
Releases: Domktorymysli/vclu
Release list
v1.6.0
Update
curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bashIt 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 aRemoteLedChannel: 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 readAlarm (CLU511000000): …, with the name taken from the Object Manager project in the pack (the bare ID when the pack has none). A CLU whoseom.luaObject 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 callsclick("<panel name>", "btn1")(alsobtn1:long,btn1:hold,swipe:up,proximity,wake), and a module describes the screens withrequire("lib/panel"): a menu that edits values in place (:toggle,:numberwithmin/max,:choice,:actionwithconfirm), 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 restoresclick()(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" }), andPanel.chart/Panel.historygive no page for an object without history. Ready widgets:menu:add(label, obj)picks the row by what the device is (itsexpose()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; andPanel.area("Salon")is a menu of whatexpose()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 withfloor = "Parter", in an order and with titles you give. As withhomekitandmqtt,expose(..., { panel = false })keeps an object off the panels andpanel = trueputs it on whatever its kind. Corners take a path and act by device (a cover opens or closes), andclock = truedraws the time. A CLU's thermostats (class 18) are imported asRemoteThermostat(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 withoutreadonly+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 mostPanel.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 inPanel.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;DrawBoxdraws 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:setRGBquoted 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/vclustarts vCLU asvclu(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 withsu-exec; a step it cannot do is logged and does not stop the start. The binary hascap_net_bind_service, so TFTP on port 69 opens without--cap-add(not with--security-opt no-new-privileges).HEALTHCHECKcalls/api/healthon the--web-portvCLU was started with. A named volume needs nothing. Check: a container run with--cap-drop ALL(or Kubernetesdrop: [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 execstill enters as root,docker exec -u vcluas vCLU's user (#3).
AI agents (MCP)
- Agents can write and test a Smart Panel module -
vclu_draft_testtakes{"click": "PAN1085", "input": "btn3"}steps, the input a CLU sends when a panel button is pressed, and the frames the panel would get show inclu_callsafter anadvance; a click no panel takes fails the step.vclu_runtime_schemadescribeslib/panel(pages, screen, Object Manager wiring),docs/ai/examples.mdhas a panel module, and the generateddocs/ai/lua-api-reference.mdlistslib/panelandRemotePanel. 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.mdnow say: readmodules/init.luaand follow its layout; a domain (a plugin, a device, a room) ismodules/<domain>/init.luawith its readings, metrics and exports, and its dashboard inmodules/dashboards/reads from that module, not from the plugin directly, so the dashboard and every export show the same values. A flatmodules/*.luais for a short standalone script, and must not sit next to a directory of the same name, whichrequirewould then never reach. Before this, an agent asked for a dashboard tended to drop a loose file inmodules/. vclu_draft_promoteno longer warns about a module'sinit.luathat is required by its directory name - promotingmodules/pogoda/init.luawarned thatmodules/init.luahas norequire('pogoda/init'), thoughrequire("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 (`...
v1.5.0
Update
curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bashIt 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.
VirtualCoverandRemoteCovernow 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 ascovermust count 0 = closed; aVirtualCoverwithoutinitialnow starts closed (0); aVirtualCoverkept withpersistand 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, andsetValue/setBrightnesswrote 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. NewgetWhite()/setWhite(0..255, ramp)for the white channel of an RGBW module. Not tested on Grenton LED hardware. - A dimmer's
setValuewith 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 withset(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 ownsetPosition,open,closeandstop(setValueonly for objects without them), positions are converted both ways, discovery announcesposition_open: 100/position_closed: 0, and the state isopenwhenever 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 asopening/closingwhile it moves (also when only the movement changed, e.g. a stop at the same position), and a roller without a position reportsstoppedwhen it stops. Contract change: the position onvclu/exposed/.../cover/.../positionis 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()andclose()of a Grenton roller with a position go toSetPosition- 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, anrgborhscolour, ON. The state no longer reports a made-up white colour. ON/OFF for lights call the object'sswitchOn/switchOff.
v1.4.0
Update
curl -sSL https://raw.githubusercontent.com/Domktorymysli/vclu/main/deploy/update.sh | sudo bashupdate.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.shupdates 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 (--forceto reinstall,--versionto 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. Withoutcurl,tarorgzipit stops before changing anything and says how to install them. It stops when the service file was edited, since the installer rewrites it (--forceto go on; keep changes insystemctl 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 -
coloron: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 stringcolorworks as before (#8).
Lua runtime
- A
VirtualNumberwith a fractional step stores the value on the step - snapping tostep = 0.1left 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:onin a timer or on every re-create withoutoffadds callbacks for ever and everyemitruns them all. At 100 callbacks for one event nameonlogs a warning with the name and the count, and again at 200, 400...; the callbacks are still added (#7). expose()warns about an object whoseonChangereturns 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, sounexpose()removes their callback (#5).
Plugins and HTTP
-
Plugins see the request error in
resp.err- the plugin docs describeresp = { status, body, headers, json, err, errCode }and plugins checkif resp.err then, but the runtime only passed the error as the callback's second argument, soresp.errwas always nil: a refused connection reached plugins as a parse error and SolaX loggedHTTP 0.resp.errnow carries the same text as the second argument (vclu-plugins#3). -
A poller warns when
done()gets a string and has noonData-done("message")is data to the poller, so the tick counts as a success andonErrornever runs. The poller logs one warning pointing todone(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 GrentonHTTPRequestobjects 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 gotHTTP 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 ofHTTP 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 7Positionas well; the position is the roller's value and its movement is read withgetState(). Home Assistant gets the position with open/closed derived from it. Import the OM file again (orPOST /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 = falseon 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.RemoteCoverpassed the value unchanged, sosetPosition(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 aRemoteCoverwith a position is that position, 0 = open .. 100 = closed, not its movement;getState()still returns the movement andgetPosition()the position. A roller without a position (class 11, or a proxy not regenerated) keeps its movement as the value andgetPosition()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 withfullIdandversion; 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
lighthandler looked for the subpathbrightness/setwhile 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 asexpose(): 0..100 in, 0..1 out, clamped to the range, throughsetBrightnesswhen the object has it (#1). - A roller published from the OM import moves to the position set in Home Assistant - the registry
coverhandler looked for the subpathposition/setwhile the topic parser already strips the trailing/set, so a position command was refused as an "unknown payload". It also looked forSetPosition/Open/Close/Stopwhile a Grenton roller has them in lower case. A position now goes throughsetPosition, clamped to 0..100 and rounded, and OPEN/CLOSE/STOP throughopen/close/stop(#25).
MQTT
MQTT:destroy()releases a client, andMQTT:new(id)with an id in use no longer leaves the old one half alive -newoverwrote the registry entry while the old client stayed connected and subscribed, anddisconnect()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 (globalMQTT, 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
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
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