AccessControl & Expose#
System eksponowania obiektów (expose) i kontroli dostępu (AccessControl) decyduje o tym, które obiekty vCLU są widoczne w Home Assistant (MQTT) i HomeKit, oraz czy można je sterować czy tylko odczytywać.
expose() - eksponowanie obiektu#
expose() rejestruje obiekt Lua w systemie integracji. Bez wywołania expose() obiekt nie pojawi się ani w HA ani w HomeKit.
local handle = expose(obj, typ, opcje)| Parametr | Typ | Opis |
|---|---|---|
obj | table | Obiekt z metodą get(0), opcjonalnie set(0, v), onChange(cb) |
typ | string | Typ eksponowania (patrz tabela niżej) |
opcje | table | Opcje (opcjonalne) |
Opcje#
| Klucz | Typ | Domyślnie | Opis |
|---|---|---|---|
name | string | - | Nazwa wyświetlana w HA/HomeKit |
id | string | - | Identyfikator do generowania ścieżki |
path | string | - | Jawna ścieżka (zamiast generowanej) |
readonly | boolean | false | Tylko odczyt |
hidden | boolean | false | Nie eksponuj (zwraca nieaktywny handle) |
mqtt | boolean | true | Eksponuj przez MQTT (Home Assistant) |
homekit | boolean | true | Eksponuj w HomeKit |
group | string | - | Grupa (do przyszłego użytku) |
area | string | - | Sugerowany pokój w Home Assistant |
Typy#
| Typ | HA | HomeKit | Opis |
|---|---|---|---|
switch | switch | Switch | przełącznik ON/OFF |
light | light | Lightbulb | lampa ON/OFF |
dimmer | light | Lightbulb | lampa z jasnością 0-100 |
cover | cover | WindowCovering | roleta z pozycją |
sensor | binary_sensor | ContactSensor | czujnik binarny |
motion | binary_sensor | MotionSensor | czujnik ruchu |
temperature | sensor | TemperatureSensor | temperatura |
humidity | sensor | TemperatureSensor | wilgotność |
number | number | Switch | wartość liczbowa |
scene | switch | Switch (bezstanowy) | scena |
button | button | - | przycisk (zdarzenie) |
lock | lock | - | zamek |
fan | fan | - | wentylator |
garage_door | cover | - | brama garażowa |
Przykłady#
-- Przekaźnik GPIO
local relay = GPIO_DOUT:new("RELAY1", 17, {activeLow = true})
expose(relay, "switch", {name = "Lampa salon"})
-- Obiekt z OM registry
expose(_:get("CLU.DOUT1"), "switch", {name = "Lampa kuchnia"})
-- Czujnik temperatury (wartość statyczna lub dynamiczna)
expose({value = 0, get = function(self) return self.value end}, "temperature", {
name = "Temperatura salon"
})
-- Scena
scene("wieczor", function()
_:get("CLU.DOUT1"):execute(DOUT.METHOD_SWITCH_ON)
_:get("CLU.DOUT2"):execute(DOUT.METHOD_SWITCH_OFF)
end)
expose(getScene("wieczor"), "scene", {name = "Tryb wieczorny"})ExposedHandle - API uchwytu#
expose() zwraca handle, przez który można dynamicznie zmieniać obiekt.
Metody fluent (chainable)#
local h = expose(obj, "switch", {name = "Lampa"})
h:name("Nowa nazwa") -- zmień nazwę
h:readonly() -- ustaw jako tylko-odczyt
h:mqttOnly() -- wyłącz HomeKit
h:homekitOnly() -- wyłącz MQTT
h:group("salon") -- ustaw grupęChaining:
expose(obj, "switch"):name("Lampa"):readonly():mqttOnly()Wartość#
h:getValue() -- odczytaj aktualną wartość
h:setValue(1) -- ustaw wartość (emituje state_changed)
-- nie działa gdy readonlyAktualizacja w runtime#
-- Zmień nazwę (aktualizuje discovery w HA i HomeKit)
h:rename("Zmieniona nazwa")
-- Zmień opcje (merge)
h:update({readonly = true})
h:update({mqtt = false}) -- ukryj z MQTT, zostaw HomeKit
h:update({homekit = false}) -- ukryj z HomeKit, zostaw MQTTUsunięcie#
h:unexpose() -- usuń obiekt z HA i HomeKit
h:isExposed() -- sprawdź czy nadal wyeksponowany (true/false)Ścieżka#
h:getPath() -- zwraca ścieżkę, np. "vclu:switch1" lub "CLU.DOU123"exposeRegistry() - automatyczne eksponowanie#
exposeRegistry() eksponuje wszystkie obiekty z registry (z OM) jednym wywołaniem:
local count = exposeRegistry()
-- Zwraca: liczba nowo wyeksponowanych obiektówReguły#
- Obiekty już wyeksponowane ręcznie przez
expose()są pomijane - ręczny expose zawsze wygrywa - AccessControl jest respektowany - ukryte obiekty nie są eksponowane
- Typ jest wykrywany automatycznie z TypeMap (DOUT → switch, DIN → sensor, ROLLER → cover, itd.)
Priorytet#
1. Ręczny expose() w user.lua (najwyższy)
2. Reguły AccessControl (środkowy)
3. exposeRegistry() automatyczne (najniższy)Jeśli w user.lua wyeksponujesz obiekt ręcznie, exposeRegistry() go nie nadpisze:
-- user.lua
expose(_:get("CLU.DOUT1"), "switch", {name = "Moja lampa", readonly = true})
-- Później (np. w bootstrap)
exposeRegistry() -- pominie CLU.DOUT1, bo już wyeksponowanyAccessControl - kontrola dostępu#
AccessControl pozwala kontrolować widoczność i sterowanie obiektów dla każdej integracji (MQTT, HomeKit) i każdego bota MCP.
Poziomy dostępu#
| Poziom | Widoczność | Sterowanie | Opis |
|---|---|---|---|
full | tak | tak | Pełny dostęp (domyślnie) |
readonly | tak | nie | Widoczny ale nie sterowalny |
hidden | nie | nie | Ukryty - nie pojawia się w integracji |
Profile#
Profile to nazwane zestawy reguł dostępu. Każdy profil ma poziom domyślny i opcjonalne nadpisania per-obiekt.
# access_control.yaml
profiles:
full-access: # wbudowany, nie można usunąć
default: full
sensors-only:
default: hidden # domyślnie ukryj wszystko
objects:
CLU.TEMP: readonly # czujniki widoczne jako readonly
CLU.HUM: readonly
lights:
default: hidden
objects:
CLU.LED1: full # światła sterowalane
CLU.LED2: full
assignments:
mqtt: [full-access] # MQTT widzi wszystko
homekit: [full-access] # HomeKit widzi wszystkoPrzypisywanie profili#
Każda integracja (MQTT, HomeKit) ma listę przypisanych profili. Boty MCP mają własne profile w .vclu.json.
MQTT → [full-access]
HomeKit → [full-access]
Bot "Claude Code" → [full-access]
Bot "Home AI" → [sensors-only, lights]Rozstrzyganie dostępu#
Gdy klient ma przypisanych kilka profili, obowiązuje zasada most permissive wins - najwyższy poziom wygrywa:
full > readonly > hiddenPrzykład: Bot “Home AI” ma profile sensors-only + lights:
| Obiekt | sensors-only | lights | Wynik |
|---|---|---|---|
CLU.TEMP | readonly | hidden | readonly |
CLU.LED1 | hidden | full | full |
CLU.LED2 | hidden | full | full |
CLU.LOCK | hidden | hidden | hidden |
Profil wbudowany#
full-access - wszystkie obiekty mają dostęp full. Nie można go edytować ani usunąć. Jest domyślnym profilem dla nowych integracji.
GUI#
Profile zarządzasz w panelu vCLU → Access Control → Profiles:
- Add Profile - nowy profil z edytorem obiektów
- Edit - tabela wszystkich obiektów z selectem poziomu dostępu
- Bulk actions - ustaw wszystkie widoczne na Full / Readonly / Hidden / Default
- Search & filter - szukaj po nazwie/ścieżce, filtruj po typie
REST API#
| Endpoint | Metoda | Opis |
|---|---|---|
/api/access/profiles | GET | Lista profili |
/api/access/profiles | POST | Utwórz/aktualizuj profil |
/api/access/profiles/:name | DELETE | Usuń profil |
/api/access/assignments | GET | Pobierz przypisania |
/api/access/assignments | POST | Zapisz przypisania |
Lua API#
-- Odczytaj dostęp (uwzględnia profile)
local level = AccessControl.get("mqtt", "CLU.DOUT1") -- "full"/"readonly"/"hidden"
-- Helpersy
AccessControl.isHidden("mqtt", "CLU.DOUT1") -- true/false
AccessControl.isReadonly("mqtt", "CLU.DOUT1") -- true/false
AccessControl.canControl("mqtt", "CLU.DOUT1") -- true = level == "full"
AccessControl.canPublish("mqtt", "CLU.DOUT1") -- true = level ~= "hidden"
-- Rozstrzyganie dla listy profili (np. bota MCP)
local level = AccessControl.resolveAccess({"sensors-only", "lights"}, "CLU.LED1")
-- → "full"
AccessControlwpływa naexposeRegistry()- decyduje co i jak zostanie wyeksponowane. Ale nie zmienia obiektów które już są wyeksponowane. Do dynamicznych zmian na żywym obiekcie używajhandle:update().
Warstwy rozstrzygania#
Trzy warstwy decydują o tym czy klient widzi i steruje obiektem:
1. expose() opcje → czy obiekt w ogóle istnieje dla integracji
2. Profile (AccessControl) → czy klient ma dostęp (full/readonly/hidden)
3. Merge profili → most permissive wins przy wielu profilachexpose() ma najwyższy priorytet. Jeśli obiekt ma mqttOnly(), to żaden profil nie udostępni go w HomeKit.
| expose() | Profil | Wynik |
|---|---|---|
| mqtt=true | full | full |
| mqtt=true | readonly | readonly |
| mqtt=true | hidden | hidden |
| mqtt=false | full | ukryty (expose wygrywa) |
| readonly=true | full | readonly (expose wygrywa) |
Scenariusze#
Ukryj obiekt z Home Assistant (MQTT), zostaw HomeKit#
-- Przy eksponowaniu
expose(obj, "switch", {name = "Lampa", mqtt = false})
-- Albo w runtime na istniejącym handle
h:update({mqtt = false})Ukryj z HomeKit, zostaw HA#
-- Przy eksponowaniu
expose(obj, "switch", {name = "Lampa", homekit = false})
-- Albo w runtime
h:update({homekit = false})Zmień z pełnego dostępu na readonly#
-- Dla obu integracji naraz
h:update({readonly = true})Całkowicie usuń obiekt z HA i HomeKit#
h:unexpose()Po unexpose():
- MQTT: publikuje pusty payload na topic discovery (HA usuwa encję)
- HomeKit: akcesorium znika po restarcie bridge
Zmień nazwę wyświetlaną#
h:rename("Nowa nazwa")
-- HA: re-publikuje discovery z nową nazwą
-- HomeKit: aktualizuje przy następnym odświeżeniuDynamicznie przełączaj dostęp w zależności od pory dnia#
local lock = expose(_:get("CLU.LOCK1"), "lock", {name = "Zamek drzwi"})
-- Scena "nocny" - wyłącz sterowanie zamkiem
scene("tryb_nocny", function()
lock:update({readonly = true})
end)
-- Scena "dzienny" - przywróć pełne sterowanie
scene("tryb_dzienny", function()
lock:update({readonly = false})
end)Ręczny expose przed exposeRegistry#
-- user.lua
-- 1. Ręcznie eksponuj z własną nazwą i ustawieniami
expose(_:get("CLU.DOUT1"), "switch", {
name = "Lampa salon",
readonly = true,
homekit = false
})
-- 2. Reszta obiektów z registry - automatycznie
exposeRegistry()
-- CLU.DOUT1 jest pominięty (już wyeksponowany ręcznie)
-- Reszta obiektów eksponowana z domyślnymi nazwamiObiekt z onChange - automatyczna aktualizacja stanu#
Jeśli obiekt implementuje onChange(callback), expose automatycznie subskrybuje zmiany:
local MySwitch = {}
function MySwitch:new()
return setmetatable({_value = 0, _cbs = {}}, {__index = self})
end
function MySwitch:get(f) return self._value end
function MySwitch:set(f, v) self._value = v; self:_notify() end
function MySwitch:onChange(cb)
table.insert(self._cbs, cb)
return function() -- MUSI zwrócić funkcję unsubscribe!
for i, c in ipairs(self._cbs) do
if c == cb then table.remove(self._cbs, i); break end
end
end
end
function MySwitch:_notify()
for _, cb in ipairs(self._cbs) do cb(self._value) end
end
local sw = MySwitch:new()
expose(sw, "switch", {name = "Mój przełącznik"})
-- Teraz sw:set(0, 1) automatycznie aktualizuje stan w HA/HomeKitMetoda
onChange()musi zwrócić funkcję unsubscribe. Bez tegounexpose()nie usunie callbacka i dojdzie do wycieku pamięci.
Tymczasowe eksponowanie#
-- Eksponuj obiekt na 5 minut (np. do testów)
local h = expose(obj, "switch", {name = "Tymczasowy"})
after(5 * 60 * 1000, function()
h:unexpose()
log.info("Tymczasowy obiekt usunięty")
end)Jak profile wpływają na exposeRegistry#
Przykład konfiguracji - MQTT ma profil full-access, HomeKit ma profil lights-only:
profiles:
full-access:
default: full
lights-only:
default: hidden
objects:
CLU.DOU0001: full
CLU.DOU0002: full
assignments:
mqtt: [full-access]
homekit: [lights-only]Wynik exposeRegistry():
| Obiekt | MQTT (full-access) | HomeKit (lights-only) | Efekt |
|---|---|---|---|
CLU.DOU0001 | full | full | expose normalnie |
CLU.DOU0002 | full | full | expose normalnie |
CLU.DOU0003 | full | hidden | expose z homekit = false (tylko MQTT) |
CLU.TEMP01 | full | hidden | expose z homekit = false (tylko MQTT) |