Skip to content

Instantly share code, notes, and snippets.

@vburlak
Created August 11, 2023 11:08
Show Gist options
  • Save vburlak/2f92c6132a1a2967feee85d18b854e87 to your computer and use it in GitHub Desktop.
Save vburlak/2f92c6132a1a2967feee85d18b854e87 to your computer and use it in GitHub Desktop.
Blinds RS-485
// 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