Шлюз SLS самодостаточен и может обходиться без внешних систем управления Умным домом. Для реализации автоматизаций, он поддерживает скриптовый язык программирования LUA. При разработке скриптов можно использовать функции как встроенные в прошивку шлюза, так и функции поддерживаемых шлюзом библиотек LUA. Текущая, поддерживаемая, версия LUA 5.4.6 (с версии прошивки 2023.10.25d1).
Немного о форматировании и названиях различных объектов шлюза.
Для работы с различными объектами используется формат кода типа zigbee.getStatus()
. В терминологии LUA это выглядит как библиотека.функция()
. Поэтому постараемся придерживаться подобного именования объектов SLS.
Форматирование текста:
- пункты меню: File -> Save
- небольшие куски кода:
print(a)
- многострочный код:
local var = 0
print(var)
Описание синтаксиса:
-- пример описания функции
result = function(var1, var2[, var3 = value])
-- Описание переменных, передаваемых в функцию и их тип
-- var1 - STR, переменная 1
-- var2 - INT, переменная 2
-- var3 - BOOL, переменная 3 (если в [квадратных] скобках, то передавать не обязательно)
-- нотация var3 = value указывает на то, что для val3 значением по умолчанию является value
-- описание результата
-- result - type, если функция что-то возвращает, описывается здесь
-- Пример реального использования
-- fn(var1, var2[, var3 = value]) описание
res = fn("sin", 0, true)
-- или
res = fn("sin", 0) -- при этом параметр var3 задан по-умолчанию и установлен в false
Примечания:
- draft - черновик или будет добавлено в будущем
- deprecated - будет удалено в будущем
Немного о модели программирования для нашего шлюза. При проектировании алгоритмов лучше не использовать функции а-ля os.delay() и самописные аналоги, выполняющие паузы в работе сценария более 1 секунды. Вместо этого лучше проектировать вызовы кусков кода из разных скриптов.
Например, часто при включении света в техническом или проходном помещении, необходимо сделать паузу и свет выключить. Первое, что приходит в голову - это сделать паузу в теле текущего сценария. Но, правильнее передать управление другому скрипту или вызвать основной рекурсивно.
Об асинхронности в программировании можно почитать, например здесь.
Все примеры скриптов собраны здесь
Редактор скриптов, по совместительству с файловым менеджером предназначен для создания, удаления и редактирования файлов, в том числе и скриптов. Найти его можно в меню Actions -> Files (в старых версиях прошивки Actions -> Scripts). Редактор разделен на несколько областей:
- Меню, с кнопками:
- Toggle files - скрыть / отобразить панель файлов
- Save - сохранить
- Run - запустить скрипт на исполнение
- Clear output - очистить панель вывода
- Панель Files. Позволяет управлять файлами:
- Создать - New file
- Удалить - значок корзины напротив имени каждого файла
- Открыть на редактирование - каждый файл представляет собой ссылку, по которой файл открывается в панели редактора
- Панель редактора
- Панель вывода - консоль для вывода результатов работы редактируемого скрипта
Скриптовый stdout
функции LUA print()
выводит информацию на Панель вывода, а также в системный лог (меню Log) шлюза. Данную функцию удобно использовать для отладки.
Для разработки или отладки скрипта, необходимо создать новый файл или открыть существующий. Например, с именем test.lua
и в него ввести код на языке LUA.
Отладка скриптов выполняется преимущественно в Редакторе скриптов SLS. Однако, некоторые пользовательские функции может быть удобнее разрабатывать во "взрослых" IDE. Например, нативный ZeroBrain Studio, VS Code, Atom и других.
Описывать типы данных всех сущностей особого смысла нет, поскольку шлюз динамично развивается и также могут меняться и типы данных. Например, до конца 2022 года тип состояний Boolean возвращался как String. Поэтому тип данных лучше проверить, дабы не было разночтений.
Для проверки типа имеется функция LUA type()
. Пример:
local var = true -- объявил локальную переменную и присвоил ей логическое значение
print(type(var)) -- выводим в STDOUT тип переменной
var = "строка" -- присвоил переменной значение типа String
print(type(var)) -- выводим в STDOUT тип переменной
-- в STDOUT получим:
-- boolean
-- string
В зависимости от задач, выполняемых той или иной автоматизацией, доступны несколько вариантов запуска скриптов:
- из скрипта инициализации
- при изменении состояния устройства
- по событию изменения объекта
- запуск из другого скрипта
- с помощью HTTP API
- периодический запуск (Таймеры)
- по подписке mqtt (в разработке).
Также, в любом месте, где прописывается имя файла скрипта, возможно писать сразу код в таком формате:
'#code', где code - тело скрипта
--Например:
scripts.setTimer('#zigbee.set("0x0123456789012345", "state", "ON")', 10, "ку")
При запуске системы выполняется скрипт инициализации init.lua
, если он есть. Перед началом работы со скриптами, рекомендуется проверить его наличие рядом со всеми остальными скриптами *.lua
. Если файла нет, то его нужно в редакторе скриптов. В init.lua
полезно инициализировать переменные для работы с устройством, а также выполнить какие либо действия. Например:
-- init.lua --
-- Уведомление в Telegram о старте шлюза --
telegram.settoken("51778***5:AAG0bvK***")
telegram.setchat("-3348***")
telegram.send("SLS загружен!!!")
Скрипт можно запускать как одно из правил SimpleBind.
Синтаксис: scriptname.lua[,Param]
Например, так может выглядеть запись SB Rule для датчика открытия
mainDoorOnLight.lua
- имя запускаемого скриптаParam
- необязательный параметр, через который в скрипт можно передать необходимые аргументы. Принимается он в скрипте через СобытиеEvent.Param
Аргументов может быть несколько. В данном примере передается 3 аргумента, разделенные символом:
: целевое устройство, которым должен управлять датчик по сработке; контролируемый статус; задержка управляющего действия. Если аргументы не прописывать, то при изменении условий, придется менять эти значения в теле скрипта. Пример похожего скрипта здесь
Также, в Simple Bind можно запускать текст скрипта:
Привязка к объекту скрипта: obj.setScript()
Выполняет текст скрипта в контексте текущего.
dofile(scriptPath)
-- scriptPath - STR, путь к запускаемому скрипту вида "/int/script.lua"
scripts.run(script[, Param])
-- script - STR, имя файла скрипта, без расширения `lua`
-- Param - STR, аргументы, передаваемые в скрипт
В прошивку шлюза встроены следующие библиотеки:
- obj. - работа с объектами
- Event. - работа с событиями
- zigbee. - управление zigbee устройствами
- mqtt. - работа с MQTT брокером
- http. - взаимодействие с внешними системами по HTTP
- telegram. - отправка уведомлений и управление шлюзом. Подробнее здесь
- os. - взаимодействие с операционной системой шлюза. Работа с хранилищем
- gpio. - управление GPIO
- audio. - управление встроенным в шлюза звуком
- net. - получение IP адресов шлюза
- yeelight. - управление устройствами Yeelight
- cloud. - работа с облаком SLS
cloud.slsys.io
Библиотека Event
служит для передачи данных в скрипт, в зависимости от того, из какой подсистемы он вызван.
События различаются типом Event.Type
. В скрипт передается числовое значение типа события, позволяющее определить источник вызова и получить различные параметры:
- Вызов по изменению состояния привязанного сенсора. Правило Simple Bind.
SCRIPT_EVENT_TYPE_STATE_UPDATE
- Вызов по изменению объекта.
SCRIPT_EVENT_TYPE_OBJ_CHANGE
- Вызов по входящему сообщению Telegram
SCRIPT_EVENT_TYPE_TLG_MESSAGE
- Таймер однократный.
SCRIPT_EVENT_TYPE_TIMEOUT
- Таймер периодический.
SCRIPT_EVENT_TYPE_INTERVAL
- Таймер Cron.
SCRIPT_EVENT_TYPE_CRON
- Вызов из LUA командой scripts.run()
SCRIPT_EVENT_TYPE_RUN
Для всех типов событий передаются следующие свойства:
Event.Type
- тип события INTEvent.Name
- имя файла вызванного скрипта с расширениемEvent.Time
- время вызова скриптаtable(sec, min, hour, day, wday, month, year)
.
Event.Param
- аргументыEvent.nwkAddr
- nwkAddr вызывающего устройстваEvent.ieeeAddr
- ieeeAddr вызывающего устройстваEvent.ModelId
- ModelId вызывающего устройстваEvent.FriendlyName
- FriendlyName вызывающего устройстваEvent.State.Name
- имя вызывающего "состояния"Event.State.Value
- текущее значение "состояния"Event.State.OldValue
- предыдущее значение "состояния"
Event.State.Value
возвращает значение с типом, заданным состоянию конвертером. Проверить можно функцией LUA type()
Event.Param
- аргументы
Event.Param
- аргументы
Event.Param
- аргументы
Служит для управления zigbee устройствами, зарегистрированными на шлюзе. Подробные примеры здесь
Возвращает статус координатора. Если запущен, вернёт 9. Начиная с версии 2022.07.24d1.
coord_status = zigbee.getStatus()
-- coord_status - INT, статус zigbee координатора. 9 - OK
Включает режим сопряжения для подключения новых устройств
zigbee.join(duration = 255[, router])
-- duration - INT, время в секундах, на которое включить Join
-- router - STR, FriendlyName, ieeeAddr или nwkAddr устройства - роутера. Если опустить этот параметр, сопряжение будет открыто для всей сети
Возвращает значения состояния устройства из кэша
result = zigbee.value(device, state)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- state - STR, состояние, значение которого необходимо получить
-- result - значение состояния
Вызывает функцию GET в конвертере. Возвращает true
в случае успеха.
result = zigbee.get(device, state)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- state - STR, состояние, значение которого необходимо получить
-- result - BOOL, true - успех, false - вероятно в конвертере нет команды GET
Устанавливает значение состояния устройства
result = zigbee.set(device, stateName, stateValue)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- stateName - STR, имя состояния, значение которого необходимо изменить
-- stateValue - значение состояние. Тип - свой для каждого значения. Например, для кнопки State:Action тип будет STR, а для яркости State:brightness тип будет INT
-- result - NIL - устройство не найдено, BOOL, true - успех, false - ошибка в имени состояния и/или его значении
Устанавливает значение состояния устройства. Можно указать тип значения (по умолчанию STR) и необходимо ли выполнять события (с версии 2022.07.24d1, по умолчанию true) .
В отличие от zigbee.set()
позволяет создавать свои состояния, виртуальные. Например, для хранения данных какого-либо состояния, в альтернативных единицах измерения. Также zigbee.set()
не позволяет изменять виртуальные состояния.
При создании состояния желательно выполнить os.save()
result = zigbee.setState(device, stateName, stateValue[[, type], events])
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- stateName - STR, имя состояния, значение которого необходимо изменить
-- stateValue - значение состояние
-- type - STR, тип значений состояния
-- events - BOOL, выполнять события (по умолчанию true)
-- result - BOOL, true - успех, false - устройство не найдено
Вариант 1
- обнулить конвертер: кнопка
Reload converter Id
на вкладкеInfo
устройства - выполнить
Action -> Save
2 раза для удаления изdevices.json
иdevices.json.bak
(устройства сохраняются 2 раза в час автоматически, поэтому если произойдет рестарт по питанию ранее 30 минут, то состояние не будет удалено)
Выриант 2
- удалить из
devices.json
иdevices.json.bak
и перезагрузить шлюз по питанию
Пример с состоянием test
До:
"st": {
"backlight_mode": "OFF",
"child_lock": false,
"current": 0,
"last_seen": 1699669806,
"linkquality": 127,
"power": 0,
"power_on_behavior": "OFF",
"state": "OFF",
"test": "0",
"trSeqNum": 150,
"voltage": 225
После:
"st": {
"backlight_mode": "OFF",
"child_lock": false,
"current": 0,
"last_seen": 1699669806,
"linkquality": 127,
"power": 0,
"power_on_behavior": "OFF",
"state": "OFF",
"trSeqNum": 150,
"voltage": 225
Изменяет таймаут опроса отдельного устройства для определения состояния online. Глобально таймауты задаются в меню Zigbee -> Config:
- Routers global timeout - период опроса роутеров (по умолчанию 10 минут)
- EndDevices global timeout - период опроса конечных устройств (по умолчанию 25 часов)
zigbee.setTimeout(device, timeout)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- timeout - INT, таймаут опроса в секундах
Для сохранения настройки после перезагрузки, добавить в init.lua.
Программное переназначение типа устройства.
zigbee.setModel(device, ModelId)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- ModelId - STR, ModelId устройства, которое поддерживается шлюзом.
В некоторых случаях протокол взаимодействия новых устройств совпадает с теми, что уже поддерживаются шлюзом SLS. В таком случае можно при загрузке шлюза подменять идентификаторы, добавив в init.lua код:
zigbee.setModel("xBox", "ptvo.switch")
Данный функционал будет полезен пользователям генератора прошивок ptvo, кто самостоятельно изменит имя устройства на кастомное.
Отправляет запрос на чтение атрибута в кластере.
zigbee.readAttr(device, epId, clusterId, AttrId[, manufId])
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- epID - INT, номер эндпоинта
-- clusterID - INT, номер кластера
-- AttrId - INT, номер атрибута
-- Например, вернуть атрибут swBuild в кластере genBasic в 1 эндпоинте:
zigbee.readAttr("0x90FD9FFFFEF7E26D", 1, 0x4000, 0x0000)
Записывает значение атрибута в кластере.
zigbee.writeAttr(device, epId, clusterId, AttrId, dataType, value[, manufId])
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- epID - INT, номер эндпоинта
-- clusterID - INT, номер кластера
-- AttrId - INT, номер атрибута
-- dataType - INT, тип данных. Например UINT8 - это 0x20
-- value - значение атрибута
Конфигурирует репортинг атрибута в кластере.
zigbee.configReport(device, epId, clusterId, AttrId, dataType, minRepInt, maxRepInt, repChange)
-- device - STR, FriendlyName, ieeeAddr или nwkAddr устройства
-- epID - INT, номер эндпоинта
-- clusterID - INT, номер кластера. Например GEN_POWER_CFG - это 0x0001
-- AttrId - INT, номер атрибута. Например BatteryVoltage - это 0x0020
-- dataType - INT, тип данных. Например UINT8 - это 0x20
-- minRepInt - INT, минимальный интервал отправки в сек
-- maxRepInt - INT, максимальный интервал отправки в сек
-- repChange - INT, минимальное значение, на которое должен измениться атрибут, для репортинга
Например:
zigbee.configReport("0x90FD9FFFFEF7E26D", 1, 0x0001, 0x0021, 0x20, 1800, 3600, 1)
-- 0x90FD9FFFFEF7E26D - ieeeAddr устройства
-- 1 - первый эндпоинт
-- 0x0001 - кластер GEN_POWER_CFG
-- 0x0021 - атрибут BatteryPercentageRemaining
-- 0x20 - тип данных UINT8
-- 1800, 3600, 1 - репортить от 30 мин до 1 часа при изменении атрибута на 1
Подробное описание здесь
Служит для отправки HTTP (HTTPS в разработке) запросов во внешние системы. Поддерживает методы GET и POST.
http.request2 (url[:port], [method, headers, body])
-- url:port - STR, URL адрес и порт целевого ресурса
-- method - STR, метод POST или GET
-- headers - STR, заголовки запроса
-- body - STR, тело запроса
Возвращает Unix время. Вызывается без параметров.
Устанавливает время шлюза
os.setTime(unixtime)
-- unixtime - STR, время в формате unixtime
Возвращает время восхода солнца (часы, минуты). Для правильной работы требуется выполнить настройки Settings -> Time & Location
os.sunrise([offset])
-- offset - INT, позволяет добавить смещение в минутах к результату вывода
-- Пример:
sunriseH, sunriseM = os.sunrise()
print("Восход солнца в " .. sunriseH .. ":" .. sunriseM )
--> Восход солнца в 10:55
Возвращает время заката солнца (часы, минуты). Для правильной работы требуется выполнить настройки Settings -> Time & Location
os.sunset([offset])
-- offset - INT, позволяет добавить смещение в минутах к результату вывода
Включает и выключает режим сна для модема WiFi. По-умолчанию выключено. Также можно заставить систему заснуть глубоким сном на time
секунд, тем самым снизив энергопотребление практически до нуля. В этом режиме не работает ничего, кроме таймера отсчета до окончания сна, по прошествии которого система перезагрузится. Это может использоваться при питании от аккумулятора.
os.setSleep(enable[,time])
-- enable - BOOL, включить = true, выключить = false спящий режим
-- time - INT, время сна в сек.
Выполняет паузу выполнения скрипта на указанное время. Не рекомендуется делать паузу более чем на 1 секунду.
os.delay(time)
-- time - INT, время паузы в миллисекундах (1 сек = 1000 мс)
Возвращает количество миллисекунд с момента загрузки системы. Вызывается без параметров.
Возвращает время с момента загрузки системы. Вызывается без параметров.
Пример:
print('Uptime: ' .. os.getUptime())
--> Uptime: 11 days 17:43:03
Возвращает статус подключения к серверу времени (NTP): true
- синхронизация с сервером NTP выполнена успешно.
os.ntp([server])
-- server - STR, адрес сервера NTP
Пример лога
[11:01:50.374] [Time] Get time from NTP: pool.ntp.org, use TimeZone: UTC+5:00
[11:01:52.422] [Time] NTP Updated! UnixTime: 1698300112, time: 26.10.2023 11:01:52, ms: 45
Возвращает количество свободной памяти в байтах.
os.freeMem([type])
-- type - STR, heap, psram
-- без параметров возвращает объем FreeHeap
Сохраняет данные. Тоже, что и меню Actions -> Save. Вызывается без параметров.
Перезагружает ОС. Вызывается без параметров.
Отправляет запросы ICMP PING на тестируемый хост. Возвращает среднее время ответа или -1 при недоступности.
os.ping(host[, count])
-- host - STR, IP или DNS адрес хоста
-- count - INT, количество запросов (по-умолчанию 1)
Включается и выключает WDT (Сторожевой таймер). Может использоваться для отладки незапланированных перезагрузок. Если шлюз перезагружен по WDT, в сериал логе появится соответствующая запись. Включен по умолчанию.
os.wdtesp(enable)
-- enable - BOOL, включить (true), выключить (false)
Управляет сторожевым таймером в модуле (RTC + WDT) SLS Hub. Без параметров возвращает время в сек. до перезагрузки. Модуль, по окончании таймера, дергает пин reset MCU.
os.wdt([time, enable])
-- enable - BOOL, включить (true), выключить (false)
-- time - INT, время в сек., на которое включается WDT
Включает логирование через UDP. Примеры получения лога
os.udplogenable(enable)
-- enable - BOOL, включить UDP лог (true); выключить (false)
Задает место хранения ресурсов web-интерфейса, для размещения на альтернативном web-сервере, например локальном. В разработке хранение web-ресурсов в хранилище SLS, для систем без выхода в интернет.
os.setAssets(url)
-- url - STR, источник ресурсов
os.led(mode, brightness, r, g, b[, effect])
-- mode - STR, режим. OFF - выключено, ON - включено, AUTO - индикация режимов/состояний шлюза (см. описание далее)
-- brightness - INT, яркость (целое, от 0 до 255)
-- r, g, b - INT, цвет (целое, от 0 до 255 или -1, если цвет менять не требуется)
-- effect - INT, включает эффекты в соответствии с таблицей
Подробнее о работе с LED здесь
Описание функций для работы с хранилищем здесь
Управление контактами ввода/вывода (GPIO) чипа ESP32.
Управление режимом контакта:
- gpio.INPUT: ввод
- gpio.OUTPUT: вывод
- gpio.INPUT_PULLUP: подтянуть к VCC
- gpio.INPUT_PULLDOWN: подтянуть к GND
gpio.mode(pin, mode)
-- pin: номер контакта
-- mode: режим контакта - gpio.INPUT, gpio.INPUT_PULLUP, gpio.INPUT_PULLDOWN, gpio.OUTPUT
Чтение сигнала с GPIO
gpio.read(pin[, ADC)
-- pin: номер контакта
-- ADC: true - чтение ADC; false - чтение цифрового сигнала
Задать каналу 2 режим входа, получить его значение:
gpio.mode(25, gpio.INPUT)
local value = gpio.read(25)
print(value)
Запись уровня в GPIO
gpio.write(pin, level)
-- pin: номер контакта
-- level: уровень - gpio.HIGH - высокий, gpio.LOW - низкий
Например, задать каналу 4 режим выхода и включить его на 100мс:
gpio.mode(27, gpio.OUTPUT)
gpio.write(27, 1)
os.delay(100)
gpio.write(27, 0)
ШИМ-контроллер ESP32 имеет 16 независимых каналов, которые можно настроить для генерации ШИМ-сигналов с различными свойствами. Все выводы, которые могут выступать в качестве выходов, могут использоваться в качестве выводов ШИМ (GPIO с 34 по 39 не могут генерировать ШИМ).
Настройка ШИМ
gpio.pwmSetup(channel, pin[, freq = 5000[, resolution = 8]])
-- chanel: канал ШИМ - 0-15
-- pin: номер контакта
-- resolution: разрешение 1-16 bits
-- freq: частота
Управление ШИМ
gpio.pwm(channel, value)
-- channel: канал 0-15
-- value: значение ШИМ
Например, задать каналу 1 режим выхода и включить ШИМ со скважностью 50%
gpio.pwmSetup(3, 32)
gpio.pwm(3, 255/100*50)
Управление звуком
audio.playurl(url) -- проигрывание звука из URL
audio.geturl() -- возвращает текущий URL
audio.stop() -- остановить проигрывание
audio.setvolume(volume_percent) -- установить уровень громкости
audio.getvolume() -- возвращает текущий уровень громкости
audio.getstatus() -- возвращает текущий статус
Возвращает адрес устройства SLS в локальной сети. Выполняется без параметров.
Возвращает внешний адрес SLS в сети интернет (если доступен). Выполняется без параметров.
Управляет устройством Yeelight. Описание протокола.
result = yeelight.send(id, method, param)
-- id - STR, IP адрес устройства
-- method - STR, команда
-- param - STR, параметры команды
-- result - JSON строка, согласно описания протокола
Возвращает статус подключения к облаку SLS cloud.slsys.io
result = cloud.isConnected()
-- result - BOOL, true шлюз подключен к облаку SLS
Функции LUA, встроенные в прошивку SLS, которые не объединены той или иной библиотекой.
Разбивает строку с помощью разделителя. Результат помещает в таблицу.
explode(separator, string)
-- string - STR, строка, которую необходимо разбить
-- separator - STR, символ разделителя
-- пример:
local string = "param1|param2|param3"
local t = explode("|", string)
local param1 = t[1]
local param2 = t[2]
local param3 = t[3]
Многие ревизии шлюзов имеют кнопку, нажатия которой можно обрабатывать скриптами. Например, для включения "режима сопряжения" при нажатии на боковую кнопку шлюза
Необходимо привязать скрипт btn_sw1.lua
zigbee.join(255, "0x0000")
и привязать его выполнение в init.lua
obj.setScript("io.input0.value", "btn_sw1.lua")
- где
io.input0.value
- номер обрабатываемого порта (в примере указана кнопка для круглого шлюза)
Более подробно вопрос с обработкой событий gpio разобран в разделе Модуля ввода-вывода
- Примеры типовых сценариев
- On-line учебник по lua
- Генератор lua скриптов на основе Blockly