Created
August 11, 2023 11:08
-
-
Save vburlak/2f92c6132a1a2967feee85d18b854e87 to your computer and use it in GitHub Desktop.
Blinds RS-485
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Backup of: https://gitlab.com/breelek/WB_Blinds_RS485 | |
// Copywright to original author: Sergey Kurakin | |
//-------- Параметры: -------- | |
var blinds_info = [ | |
{ | |
name: 'Dooya DM35', // Имя устройства в веб-интерфейсе. | |
group: 1, // Номер группы, заданный при привязке к Blinds-RS485. | |
id: 2, // Номер мотора в группе, заданный при привязке устройства к Blinds-RS485. | |
}, | |
{ | |
name: 'Dooya DT82', // Имя устройства в веб-интерфейсе. | |
group: 1, // Номер группы, заданный при привязке к Blinds-RS485. | |
id: 3, // Номер мотора в группе, заданный при привязке устройства к Blinds-RS485. | |
}, | |
{ | |
name: 'Dooya All', // Имя устройства для управления всеми моторами в веб-интерфейсе. | |
group: 0, // Не нужно менять это значение. Оно задано в протоколе. Означает, что команда предназначена для всех устройств. | |
id: 0, // Не нужно менять это значение. Оно задано в протоколе. Означает, что команда предназначена для всех устройств в группе. | |
}, | |
] | |
var socket_port = 8125 // Если чем-то не устраивает, можно изменить. | |
var polling_interval = 1000 // Интервал опроса мотора. 1000 = 1 сек. | |
var debug = true // Выводить/не выводить доп. информацию в journalctl. | |
//----------- Код: ----------- | |
var port | |
var device_name_pattern = 'blinds_rs485_{}_{}' | |
var mot_resp = /55( [\da-f]{2}){7}/gmi | |
extract_port_name() | |
/* Если в веб-интерфейсе и логах увидите сообщение 'Blinds RS485 not found', | |
** закоментрируйте вызов extract_port_name() и задайте порт раскоментировав | |
** строку ниже. Вместо ttyACM0 подставте своё значение, если оно другое. | |
*/ | |
//port = '/dev/ttyACM0' // Номер порта, к которому подключен Blinds RS485. | |
function extract_port_name() { | |
var cmd = "dmesg|perl -00nE 'print \$2 if /usb (\\d[\\d|\\.|\\-]+:).+Blinds RS485[\\s\\S]+\$1\\d.+: (ttyACM\\d+)/'" | |
runShellCommand(cmd, { | |
captureOutput: true, | |
exitCallback: function (exitCode, capturedOutput) { | |
if (capturedOutput && capturedOutput.length) { | |
port = '/dev/{}'.format(capturedOutput) | |
} else { | |
debug && port = 'Blinds RS485 not found' | |
} | |
debug && log('port: ' + port) | |
} | |
}) | |
} | |
/* Для отправки команд нам нужна утилита socat. По-умолчанию она не установлена. | |
** Код ниже проверяет установлен ли socat и если нет, то выполняет установку. | |
** Wiren Board должен в этот момент иметь подключение к internet. | |
*/ | |
check_socat() | |
/* Сообщения можно посмотреть через journalctl: | |
** journalctl -u wb-rules -f | |
*/ | |
function check_socat() { | |
var cmd = 'which socat || apt-get install socat -y' | |
runShellCommand(cmd, { | |
captureOutput: true, | |
exitCallback: socat_checked | |
}) | |
} | |
function socat_checked(exitCode, capturedOutput) { | |
if (exitCode === 0) { | |
debug && log('Socat OK.') | |
//launch_tcp2serial_relay() | |
await_port_extraction(3) | |
} else { | |
log("Can't install socat: " + capturedOutput) | |
} | |
} | |
function await_port_extraction(num_checks) { | |
if (port) { | |
debug && log('Launch tcp to Blinds-RS485 relay') | |
launch_tcp2serial_relay() | |
} else if(!num_checks) { | |
debug && log("Can't extract Blinds-RS485 port") | |
} else { | |
debug && log('Await port extraction. Attempt ' + num_checks) | |
setTimeout(await_port_extraction, 500, --num_checks) | |
} | |
} | |
function launch_tcp2serial_relay() { | |
var cmd = 'socat -T0.05 tcp-l:{},reuseaddr,fork {},cr,echo=0 &'.format(socket_port, port) | |
runShellCommand(cmd, { | |
captureOutput: true, | |
exitCallback: relay_launched | |
}) | |
} | |
function relay_launched(exitCode, capturedOutput) { | |
if (exitCode === 0) { | |
debug && log('Serial port to tcp relay launched: ' + capturedOutput) | |
} else { | |
log("Can't launch tcp relay: " + capturedOutput) | |
} | |
} | |
function Blind_Widget(name) { | |
this.title = name | |
this.cells = { | |
port: { | |
type: 'control', | |
value: '-', | |
order: 1, | |
}, | |
state: { | |
type: 'control', | |
value: '-', | |
order: 2, | |
}, | |
position: { | |
type: 'control', | |
value: '-', | |
order: 4, | |
}, | |
cover: { | |
type : 'range', | |
value : 0, | |
min: 0, | |
max : 100, | |
order: 5, | |
}, | |
open: { | |
type: 'pushbutton', | |
order: 6, | |
}, | |
stop: { | |
type: 'pushbutton', | |
order: 7, | |
}, | |
close: { | |
type: 'pushbutton', | |
order: 8, | |
} | |
} | |
} | |
/* Для каждого элемента в blinds_info создаём виртуальное устройство, | |
** к которому будем обращаться по имени device_name, | |
** которое будет отображаться в веб-интерфейсе как значение, указанное в name, | |
** с нужными элементами управления и отображения состояния: | |
*/ | |
blinds_info.forEach(function(blind) { | |
var group = blind.group | |
var id = blind.id | |
blind.device_name = device_name_pattern.format(group, id) // Название устройства в "Каналы MQTT". | |
defineVirtualDevice(blind.device_name, new Blind_Widget(blind.name)) | |
// Правило для range: | |
defineRule('dooya_rs485_range_{}_{}'.format(group, id), { | |
whenChanged: '{}/cover'.format(blind.device_name), | |
then: function () { | |
var cmd = [ | |
group, | |
id, | |
'close% {}'.format(dev[blind.device_name]['cover']) | |
].join(' ') | |
runShellCommand (command(cmd)) | |
} | |
}) | |
// Обернем создание однотипных правил в функцию: | |
function control_btn_action(cmd) { | |
defineRule('dooya_rs485_{}_{}_{}'.format(cmd, group, id), { | |
whenChanged: '{}/{}'.format(blind.device_name, cmd), | |
then: function () { | |
runShellCommand (command([group, id, cmd].join(' '))) | |
} | |
}) | |
} | |
// Каждой кнопоке виртуального устройства зададим действие: | |
control_btn_action('open') | |
control_btn_action('stop') | |
control_btn_action('close') | |
}) | |
// Шаблон shell-комнды: | |
function command(cmd) { | |
var composed_cmd = 'echo "{}"|socat - tcp:localhost:{}'.format(cmd, socket_port) | |
//debug && log('Composed cmd: ' + composed_cmd) | |
return composed_cmd | |
} | |
//--- Формирование команды опроса состояния моторов: --- | |
// Список команд в опросе: | |
var cmd_list = [ | |
{ | |
cmd: 'position?', | |
handler: position_show, | |
}, | |
{ | |
cmd: 'motor_status?', | |
handler: state_show, | |
}, | |
] | |
var motors = blinds_info.filter(function(motor) { | |
return ((motor.group && motor.id) > 0) | |
}) | |
var scan_motors_cmd = motors.map(function(motor) { | |
return cmd_list.map(function(cmd) { | |
return [motor.group, motor.id, cmd.cmd].join(' ') | |
}) | |
}).join(',').split(',').join('\n') // array[][].flatten.toString(s) | |
//--- Отображение состояния моторов: --- | |
// Отображение положения шторы в веб-интерфейсе: | |
function position_show(device_name, resp) { | |
var pos = parseInt(resp.slice(-8,-6), 16) | |
if (pos < 101) { | |
dev[device_name]['position'] = 'Opened {} %'.format(pos) | |
} else { | |
dev[device_name]['position'] = 'It might be necessary to set limits' | |
} | |
} | |
// Отображение состояния мотора в веб-интерфейсе: | |
function state_show(device_name, resp) { | |
var states = { | |
'00': 'stopped', | |
'01': 'opening...', | |
'02': 'closing...', | |
'03': 'setting mode', | |
} | |
if (resp) { // if do not check, we will get an error in the logs | |
dev[device_name]['state'] = '{}'.format(states[resp.slice(-8,-6)]) | |
} | |
} | |
// Обработка ответов команд. | |
function response_processing(_, captureOutput) { // Checking an exitCode from bash is meaningless | |
var responses = captureOutput.match(mot_resp) | |
while (responses && responses.length) { // just responses.length gives an error in logs when usb driver fault | |
var motor_id = responses[0].slice(3,8) | |
var id = parseInt(motor_id.slice(0,2), 16) | |
var group = parseInt(motor_id.slice(3), 16) | |
var device_name = device_name_pattern.format(group, id) // Название устройства в "Каналы MQTT". | |
var mot_resps = responses.filter(function(resp) { | |
return resp.slice(3,8) === motor_id | |
}) | |
cmd_list.forEach(function(item, index) { | |
item.handler(device_name, mot_resps[index]) | |
}) | |
responses = responses.filter(function(resp) { | |
return resp.slice(3,8) !== motor_id | |
}) | |
} | |
} | |
//--- Сканирование моторов: --- | |
var motor_polling = setInterval(function () { | |
runShellCommand(command(scan_motors_cmd), { | |
captureOutput: true, | |
exitCallback: response_processing, | |
}) | |
}, polling_interval) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment