Китайские кондиционеры с портом X-Y-E

Подключение кондиционеров или фанкойлов с портом X-Y-E (Midea, Lessar, MDV, и многие другие)
1 - на плате кондиционера настраиваем адрес.
2 - подключаем к контроллеру к порту RS485. X - это A, Y - это B, E - это GND. (порт который будите использовать должен быть свободным, поэтому его нужно удалить с Настроек драйвера serial-устройств /etc/wb-mqtt-serial.conf)
3 - копируем файлы (mdv и libSystem.IO.Ports.Native.so) в папку usr/bin. Файлу mdv нужно выдать разрешение на запуск, без этого работать не будет.
4 - В правилах создаем новый файл, и копируем туда содержимое с MDV.js, предварительно настроив, сохраняем. После этого в вебе появляться все кондиционеры или фанкойлы которые были настроены в MDV.js.

Настройка MDV.js
Первая строка это массив с ID кондиционеров. Можно в любом порядке писать числа, главное без повторений от 0 до 63.
93 строка - это собственно команда на запуск программы. В конце этой команды есть строчка /dev/ttyRS485-1 - это RS485 порт к которому мы подключили кондиционеры.

После запуска программа начнет работать примерно через 5 секунд.
В вебе появиться устройство с именем Статус опроса кондиционеров. Если все хорошо, то в топике sist/GanGetID будут отображаться ID кондиционеров которые опрашиваются.

Описание топиков -
Temp - температура из кондиционера.
Alarm - 0 - норма, 1 - в кондиционере ошибка, 2 - кондиционер не отвечает.
AlarmCode - 0 - нет ошибок.
1 - E0 - Перефазировка или отсутствие фазы
2 - E1 Ошибка связи
3 - Е2 Ошибка датчика Т1
4 - Е3 Ошибка датчика Т2А
5 - Е4 Ошибка датчика Т2В
6 - Е5 Ошибка датчика температуры нагнетателя компрессора Т3/Т4
7 - Е6 Ошибка несущей частоты (ошибка связи)
8 - Е7 Ошибка EEPROM
9 - E8 - Ошибка вентилятора, нет определения скорости вентилятора
10 - E9 Ошибка связи между платой управления и платой индикации
11 - ЕA Перегрузка компрессора
12 - ЕB Защита модуля инвертора
13 - ЕC Ошибка очистки
14 - ЕD Защита наружного блока
15 - ЕE Защита от протечки конденсатора
16 - ЕF Прочие ошибки
Power - 0 - Выкл, 1 - вкл.
Mode - 0 - охлаждение, 1 - обогрев, 2 - вентилятор, 3 - осушение, 4 - авто
Speed - Скорость вентилятора 0 - выкл., 1 - мин., 2 - сред., 3 - макс., 4 - авто
SetTemp - Устаква температуры - 16 - 33 градуса.
Blinds - Управление поворотом жалюзи если есть. 0 - выкл, 1 - вкл.

Ссылка на файлы - MDV — Яндекс Диск

6 лайков

Восхищен, да. Большая работа!
И очень приятно что делитесь.

а можно по модельно гдето посмотреть какие именно с портом X-Y-E?
то есть я хочу купить кондиционеры, но у продавца точнно не написано если ли поддержка этого порта.
в инструкциях на сайте продажников такой инфы нет.

Это очень сложный вопрос. Платы с таким разъемом устанавливаются на разных кондиционерах, при этом не на всех эти разъемы распаяны. Если не распаяны, значит сетевая карта тоже не распаяна. Есть такие варианты где сетевая карта отдельно подключается. Во всяком случае это нужно уже самому определять. Вообще этот разъем используется для централизованного управления кондиционерами или фанкойлами с одного проводного пульта.
Искать информацию нужно в документации к оборудованию. Например во вложении я приложил блок управления для фанкойлов который имеет и клемы XYE и Модбас PEQ. PEQ - это обычно разъем для подключения мультизональной системы кондиционирования.
Конкретно модели - сложно назвать, так как даже в одной серии могут стоять разные платы управления.
Ориентироваться нужно на устройства с централизованным управлением, и смотреть документацию.
IOM DF-KZ03-04 rus (2).pdf (664,3 КБ)

1 лайк

Вот еще маленькая программа для тестирования шины XYE. При запуске нужно выбрать COM порт к которому подключен USB конвертер на RS485. После выбора порта в консоли будет отображаться опрос всех возможных 64 устройств от 0 до 64. При отправке запроса в консоль выводиться текст - Отправка запроса и ID устройства которое опрашивается.
Если ответа нет, устройства с таким ID нет. Если такой ID существует, то в консоль выведется ответ из 30 байт и строка - Устройство онлайн плюс ID устройства
Файл здесь - MDV — Яндекс Диск
Программа для виндовс x64

2 лайка

@sansa26 Спасибо за исследовательскую работу и за то что поделились результатом.

Планирую покупать кондиционеры, пока Midea VRF выглядят наиболее привлекательно по цене, однако не нахожу нормальной возможности подключить к ним из wirenboard, поэтому очень интересен ваш способ. У меня задача управлять 6 внутренними Midea VRF однопоточными кассетными блоками.

Помогите разобраться, пожалуйста

  1. На сколько я понимаю, все 6 блоков можно соединить RS485 подключить на отдельный вход RS485 на WB и общаться с ними из WB?
  2. Можно ли настроить wb-mqtt-serial так чтобы общаться с кондиционерами через mqtt? В конечном счете хочется писать правила на wb-rules, а так же реализовать интерфейс для HMI панели, которая работает через WB mqtt брокер
  3. Правильно ли я понимаю “mdv” (из приложенных файлов) это самописная программа для того чтобы WB мог записать сообщение в шину RS485? Можете поделиться исходным кодом?
  4. “mdvt.exe” - самописная программа для Windows, которая по USB-конвертеру-RS485 опрашивает шину с перебором всех возможных устройств? Можете поделиться исходным кодом? У меня только Мак, хочу собрать под него
  5. Разбирались ли вы как выдается адрес конкретному блоку. Если из всего 64, и они присваиваются на заводе, то велик шанс коллизии и нужно проверять адреса при покупки блоков? Если нет, то могут ли они меняться со временем? Вы пишете про настройку адресов на каждой плате, как это сделать?
  6. Можете рассказать как вы отправляете запрос через node-red
  7. Вы писали про то, что не получается корректно получить текущий режим, если это Авто. В текущем решении этой проблемы нет? Есть ли другие несостыковки?

Заранее благодарю за ответы

Кому интересно делюсь информацией, которую нашел

  1. Midea “Шлюз GW-KNX для интеграции VRF” - $300 на каждый блок
  2. Midea “Шлюз GW-MOD” - $3000 вешается на внешний блок, работает через modbus. Вроде такой же есть у Intesis со схожей ценой
  3. Intesis “MIDEA COMMERCIAL & VRF SYSTEMS TO MODBUS RTU INTERFACE” - modbus на каждый внутренний блок, цена вроде $300
  4. CoolMasterNet “Universal HVAC Gateway” $2500 вешается на внешний блок и умеет работать с разными производителями
  5. Midea ССМ-15 - $300 не для интеграции в умный дом, но дает возможность управлять кондиционерами через Midea облако. Вешается на внешний блок
  6. Есть еще Onokom, но они вроде Midea не поддерживают. Стоят тоже прилично 20к руб на каждый блок (статья на wb)
  7. CCM18 (см UPD ниже)

Девайсы 2 и 4 используются для автоматизаций целых зданий, в рамках этой задачи они практически бесплатны, но для дома это оверкилл
Девайсы 1, 3 подходят но стоят по $300

Я еще детально не изучал, но обратите внимание, что часть оборудования похоже подходят только для VRF систем, в то время как, для бытовых нужд чаще используют Сплит или Мультисплит.
Сплит - один наружный блок соединено с одним внутренним блоком
Мультисплит - один наружный блок с отдельной трассой до каждого внутреннего блока
VRF - один наружный блок с общей трассой для всех внутренних блоков

Для примера, у Haier есть дешевый родной модбас конвертер на каждый блок YCJ-A002 за 5000руб, но сама линийка кондиционеров стоит дороже.

UPD
Оказывается есть еще CCM18 (можно почитать тут), пока выглядит многообещающим - умеет работать по Modbus RTU и стоит в районе $300

Привет!

  1. Для того чтоб блоки можно было подключить, нужно чтобы у них на платах был разъем X Y E. Не на всех платах его распаивают. Подключается через порт RS 485. Общаться с ними можно, только с одним нюансом, из правил нужно обращается к кондиционеру с такой командой publish("/Кондиционер_1/dev/SetTemp/on", "23"); в место dev["Кондиционер_1/SetTemp"] = 23
  2. wb-mqtt-serial это для стандартных протоколов которые поддерживает контроллер. В данном случае он не нужен. Когда заведешь кондиционеры через мою программу, они и так будут в MQTT, поэтому можно общаться через топики.
  3. mdv это самописная программа на C# .net6 bkb .net7. Исходник выложу завтра, там нет ничего секретного.
  4. mdvt.exe - это такая же программа, просто урезанная, и в данном случае скомпилированная для Виндовс. Исходник завтра скину.
  5. Адрес устройству выставляется вручную, согласно инструкции к кондиционеру (физически)
    6 Через node-red - я там использовал модуль который позволяет отправлять и считывать данные с ком порта. Это плохой вариант, так как там сложно с таймаутами. JS не самый хороший вариант для таких задач.
  6. Это завтра посмотрю, вспомню.
1 лайк

Это программа для контроллера:

namespace mdv
{
    using System.IO.Ports;
    using Timer = System.Timers.Timer;
    using System.Text.Json;
    using System.Net;
    using uPLibrary.Networking.M2Mqtt.Messages;
    using uPLibrary.Networking.M2Mqtt;
    using System.Text;
    using static System.Text.Encoding;
    using System.Collections.Generic;
    using System;
    using System.Linq;

    internal class Program
    {
        private static readonly Timer TimerWrite = new();
        private static readonly Timer TimerNoData = new();
        private static SerialPort port;
        static int[] enter;
        static int IDfan = 0;
        static string[] Topics;
        static Byte[] QOSMQTT;
        static readonly Dictionary<int, Byte[]> FansGet = new();
        static readonly Dictionary<int, Byte[]> FansSet = new();
        static readonly Queue<int> SetNum = new(16);
        static Byte[] indata;
        static readonly MqttClient client = new(IPAddress.Parse("127.0.0.1")); //   127.0.0.1 192.168.42.1
        static void Main(string[] args)
        {
            enter = JsonSerializer.Deserialize<int[]>(args[0]);
            Topics = new string[enter.Length * 10];
            QOSMQTT = new Byte[enter.Length * 10];
            for (int i = 0; i < enter.Length; i++)
            {
                FansGet[enter[i]] = new Byte[16] { 170, 192, (Byte)enter[i], 0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 63, (Byte)(129 - enter[i]), 85 };
                FansSet[enter[i]] = new Byte[16] { 170, 195, (Byte)enter[i], 0, 128, 0, 8, 128, 21, 0, 0, 0, 0, 60, 0, 85 };
                Topics[0 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Power/on";
                Topics[1 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Mode/on";
                Topics[2 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Speed/on";
                Topics[3 + i * 10] = $"/devices/Fan_{enter[i]}/controls/SetTemp/on";
                Topics[4 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Blinds/on";
                Topics[5 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Power";
                Topics[6 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Mode";
                Topics[7 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Speed";
                Topics[8 + i * 10] = $"/devices/Fan_{enter[i]}/controls/SetTemp";
                Topics[9 + i * 10] = $"/devices/Fan_{enter[i]}/controls/Blinds";
            }

            client.MqttMsgPublishReceived += client_MqttMsgPublishReceived;

            string clientId = Guid.NewGuid().ToString();
            client.Connect(clientId);
            client.Subscribe(Topics, QOSMQTT);

            //string strValue = Convert.ToString(44);  client.Unsubscribe(new string[]);

            static void client_MqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e)
            {
                //Console.WriteLine($"{e.Topic}, - {Default.GetString(e.Message)}");
                if (e.Topic.Split('/').Length == 6)
                {
                    if (e.Topic.Split('/')[4] == "Power")                                 // Включить выключить
                    {
                        if (Default.GetString(e.Message) == "1")
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] < 128)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] += 128;
                            }
                        }
                        else if (Default.GetString(e.Message) == "0")
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] -= 128;
                            }
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Mode")                   // Режим работы 0 - холод, 1 - тепло, 2 - вент, 3 - сушка, 4 - авто
                    {

                        if (Default.GetString(e.Message) == "0") // Режим охлаждение
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 136;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 8;
                            }
                        }
                        else if (Default.GetString(e.Message) == "1") // Режим Обогрев
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 132;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 4;
                            }
                        }
                        else if (Default.GetString(e.Message) == "2") // Режим Осушение
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 130;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 2;
                            }
                        }
                        else if (Default.GetString(e.Message) == "3") // Режим вентилятора
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 129;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 1;
                            }
                        }
                        else if (Default.GetString(e.Message) == "4") // Режим Авто
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 144;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 16;
                            }
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Speed")                   // Скорость вентилятора 1, 2, 3, 4 - авто
                    {

                        if (Default.GetString(e.Message) == "1")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 4;
                        }
                        else if (Default.GetString(e.Message) == "2")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 2;
                        }
                        else if (Default.GetString(e.Message) == "3")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 1;
                        }
                        else if (Default.GetString(e.Message) == "4")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 128;
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Blinds")                   // Поворот жалюзи
                    {
                        if (Default.GetString(e.Message) == "1")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][9] = 4;
                        }
                        else if (Default.GetString(e.Message) == "0")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][9] = 0;
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "SetTemp")                   // Уставка температуры 16-32
                    {
                        if (int.Parse(Default.GetString(e.Message)) > 15 & int.Parse(Default.GetString(e.Message)) < 32)
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][8] = Byte.Parse(Default.GetString(e.Message));
                        }
                    }
                    if (!SetNum.Contains(int.Parse(e.Topic.Split('/')[2].Split('_').Last()))) // Добавить в очередь
                    {
                        SetNum.Enqueue(int.Parse(e.Topic.Split('/')[2].Split('_').Last()));
                    }
                }
                else if (e.Topic.Split('/').Length == 5)
                {
                    if (e.Topic.Split('/')[4] == "Power")                                 // Включить выключить
                    {
                        if (Default.GetString(e.Message) == "1")
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] < 128)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] += 128;
                            }
                        }
                        else if (Default.GetString(e.Message) == "0")
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] -= 128;
                            }
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Mode")                   // Режим работы 0 - холод, 1 - тепло, 2 - вент, 3 - сушка, 4 - авто
                    {

                        if (Default.GetString(e.Message) == "0") // Режим охлаждение
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 136;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 8;
                            }
                        }
                        else if (Default.GetString(e.Message) == "1") // Режим Обогрев
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 132;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 4;
                            }
                        }
                        else if (Default.GetString(e.Message) == "2") // Режим Осушение
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 130;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 2;
                            }
                        }
                        else if (Default.GetString(e.Message) == "3") // Режим вентилятора
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 129;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 1;
                            }
                        }
                        else if (Default.GetString(e.Message) == "4") // Режим Авто
                        {
                            if (FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] > 127)
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 144;
                            }
                            else
                            {
                                FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][6] = 16;
                            }
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Speed")                   // Скорость вентилятора 1, 2, 3, 4 - авто
                    {

                        if (Default.GetString(e.Message) == "1")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 4;
                        }
                        else if (Default.GetString(e.Message) == "2")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 2;
                        }
                        else if (Default.GetString(e.Message) == "3")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 1;
                        }
                        else if (Default.GetString(e.Message) == "4")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][7] = 128;
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "Blinds")                   // Поворот жалюзи
                    {
                        if (Default.GetString(e.Message) == "1")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][9] = 4;
                        }
                        else if (Default.GetString(e.Message) == "0")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][9] = 0;
                        }
                    }
                    else if (e.Topic.Split('/')[4] == "SetTemp")                   // Уставка температуры 16-32
                    {
                        if (int.Parse(Default.GetString(e.Message)) > 15 & int.Parse(Default.GetString(e.Message)) < 32)
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][8] = Byte.Parse(Default.GetString(e.Message));
                        }
                        else if (Default.GetString(e.Message) == "0")
                        {
                            FansSet[int.Parse(e.Topic.Split('/')[2].Split('_').Last())][9] = 0;
                        }
                    }
                    client.Unsubscribe(new string[] { e.Topic });
                }





            }

            port = new SerialPort
            {
                PortName = args[1],
                BaudRate = 4800,
                DataBits = 8,
                Parity = System.IO.Ports.Parity.None,
                StopBits = System.IO.Ports.StopBits.One,
                ReadTimeout = 100,
                WriteTimeout = 30,
                ReadBufferSize = 32,
                WriteBufferSize = 16
            };
            port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler2);
            port.ErrorReceived += new SerialErrorReceivedEventHandler(Eror);
            port.PinChanged += new SerialPinChangedEventHandler(Stopp);
            try
            {
                port.Open();
                client.Publish($"/devices/sist/controls/Serial", Encoding.UTF8.GetBytes("Порт открыт"), 0, false);
            }
            catch (Exception e)
            {
                //Console.WriteLine("ERROR: невозможно открыть порт:" + e.ToString());
                client.Publish($"/devices/sist/controls/Serial", Encoding.UTF8.GetBytes("Невозможно открыть порт"), 0, false);
                return;
            }


            TimerNoData.Interval = 200;
            TimerNoData.Elapsed += NoConect;
            TimerNoData.Elapsed += Writers;
            TimerNoData.AutoReset = false;
            TimerNoData.Enabled = false;

            TimerWrite.Interval = 140;
            TimerWrite.Elapsed += Writers;
            TimerWrite.AutoReset = false;
            TimerWrite.Enabled = true;

            Console.ReadLine();
        }

        static void Eror(
                object sender,
                SerialErrorReceivedEventArgs e)
        {
            //Console.Write($"[eror]");
            client.Publish($"/devices/sist/controls/Error", Encoding.UTF8.GetBytes($"Ошибка - {e.ToString}"), 0, false);
        }

        static void Stopp(
                object sender,
                SerialPinChangedEventArgs e)
        {
            //Console.Write($"[stopp]");
            client.Publish($"/devices/sist/controls/Error", Encoding.UTF8.GetBytes("Неизвестное событие"), 0, false);
        }
        static bool keyRepeat = false;
        static bool keySet = false;
        static int RepeatCounter = 0;
        static int t = 0;
        static int s = 0;
        static int n = 0;
        static void Writers(Object? source, System.Timers.ElapsedEventArgs? e) //  отправка данных
        {
            TimerNoData.Enabled = true;
            if (keyRepeat & RepeatCounter < 2)
            {
                s = 0;
                for (int i = 0; i < 14; i++)
                {
                    s += FansSet[n][i];
                }
                s += 85;
                FansSet[n][14] = (Byte)(255 - s % 256);
                port.Write(FansSet[n], 0, 16);
                //Console.WriteLine($"Повторная уставка {FansSet[n][2]}");
                RepeatCounter += 1;
            }
            else if (SetNum.Count > 0)
            {
                if (RepeatCounter > 0)
                {
                    RepeatCounter = 0;
                }
                if (!keySet)
                {
                    keySet = true;
                }
                s = 85;
                n = SetNum.Dequeue();
                for (int i = 0; i < 14; i++)
                {
                    s += FansSet[n][i];
                }

                FansSet[n][14] = (Byte)(255 - s % 256);
                port.Write(FansSet[n], 0, 16);
                //Console.WriteLine($"Уставка {FansSet[n][2]}, вент - {FansSet[n][7]}");
            }
            else
            {
                if (keySet)
                {
                    keySet = false;
                }
                port.Write(FansGet[enter[t]], 0, 16);
                //Console.WriteLine($"Отправка запроса {FansGet[enter[t]][2]}");
                IDfan = enter[t];
                client.Publish($"/devices/sist/controls/GanGetID", Encoding.UTF8.GetBytes($"{IDfan}"), 0, false);
                if (enter.Length != 1)
                {
                    if (t < enter.Length - 1)
                    {
                        t += 1;
                    }
                    else
                    {
                        t = 0;
                    }
                }
            }
        }

        static void NoConect(Object? source, System.Timers.ElapsedEventArgs? e) // нет связи
        {
            //Console.WriteLine("Нет ответа");

            if (keySet)
            {
                keyRepeat = true;
            }
            else
            {
                client.Publish($"/devices/Fan_{IDfan}/controls/Alarm", Encoding.UTF8.GetBytes("2"), 0, false);

            }
        }


        /// //////////////////////////////////////////////////////////////////////////////////////////////////
        static int s2 = 0;
        static void DataReceivedHandler2(
                        object sender,
                        SerialDataReceivedEventArgs e)
        {
            SerialPort sp = (SerialPort)sender;
            if (sp.BytesToRead > 30)
            {
                TimerNoData.Enabled = false;
                if (TimerWrite.Enabled == false)
                {
                    TimerWrite.Enabled = true;
                }

                indata = new Byte[32];
                sp.Read(indata, 0, 32);


                s2 = 85;
                if (indata[0] == 254)
                {
                    indata = indata.Skip(1).ToArray();
                }
                for (int i = 0; i < indata.Length - 2; i++)
                {
                    s2 += indata[i];
                    //Console.WriteLine($"[{indata[i]}]");
                }


                if ((int)indata[30] == 255 - s2 % 256)
                {

                    //Console.WriteLine($"ОК {indata[4]}");
                    DataPars(indata);
                }
                else
                {
                    if (keySet)
                    {
                        keyRepeat = true;
                    }
                    client.Publish($"/devices/sist/controls/Error", Encoding.UTF8.GetBytes("Ошибка данных"), 0, false);
                    //Console.WriteLine("Не ОК");
                }
                sp.DiscardInBuffer();
            }
            else
            {
                sp.DiscardInBuffer();
            }


        }
        static void DataPars(Byte[] data)
        {
            client.Publish($"/devices/Fan_{data[4]}/controls/Power", Encoding.UTF8.GetBytes($"{(data[8] & 128) >> 7}"), 0, false);

            if ((data[8] & 16) == 16) // Режим авто
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Mode", Encoding.UTF8.GetBytes("4"), 0, false);
            }
            else if ((data[8] & 8) == 8) // Режим охлаждения
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Mode", Encoding.UTF8.GetBytes("0"), 0, false);
            }
            else if ((data[8] & 4) == 4) // Режим обогрева
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Mode", Encoding.UTF8.GetBytes("1"), 0, false);
            }
            else if ((data[8] & 2) == 2) // Режим осушения
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Mode", Encoding.UTF8.GetBytes("2"), 0, false);
            }
            else if ((data[8] & 1) == 1) // Режим вентилятора
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Mode", Encoding.UTF8.GetBytes("3"), 0, false);
            }

            if ((data[9] & 128) == 128) // Скорость 0 авто
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/SpeedST", Encoding.UTF8.GetBytes("4"), 0, false);
            }
            else if ((data[9] & 4) == 4)  // Скорость 1
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/SpeedST", Encoding.UTF8.GetBytes("1"), 0, false);
            }
            else if ((data[9] & 2) == 2) // Скорость 2
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/SpeedST", Encoding.UTF8.GetBytes("2"), 0, false);
            }
            else if ((data[9] & 1) == 1) // Скорость 3
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/SpeedST", Encoding.UTF8.GetBytes("3"), 0, false);
            }
            else if (data[9] == 0) // Скорость 0
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/SpeedST", Encoding.UTF8.GetBytes("0"), 0, false);
            }


            client.Publish($"/devices/Fan_{data[4]}/controls/SetTemp", Encoding.UTF8.GetBytes($"{data[10]}"), 0, false);
            client.Publish($"/devices/Fan_{data[4]}/controls/Temp", Encoding.UTF8.GetBytes($"{(data[11] / 2) - 20}"), 0, false);

            if ((data[8] & 4) == 4) // Жалюзи включены
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Blinds", Encoding.UTF8.GetBytes("1"), 0, false);
            }
            else if ((data[8] & 4) == 0) // Жалюзи выключены
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Blinds", Encoding.UTF8.GetBytes("0"), 0, false);
            }

            if (data[22] == 0 & data[23] == 0)
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Alarm", Encoding.UTF8.GetBytes("0"), 0, false);
                client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("0"), 0, false);
            }
            else
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Alarm", Encoding.UTF8.GetBytes("1"), 0, false);
                if ((data[22] & 1) == 1)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("1"), 0, false);
                }
                else if ((data[22] & 2) == 2)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("2"), 0, false);
                }
                else if ((data[22] & 4) == 4)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("3"), 0, false);
                }
                else if ((data[22] & 8) == 8)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("4"), 0, false);
                }
                else if ((data[22] & 16) == 16)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("5"), 0, false);
                }
                else if ((data[22] & 32) == 32)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("6"), 0, false);
                }
                else if ((data[22] & 64) == 64)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("7"), 0, false);
                }
                else if ((data[22] & 128) == 128)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("8"), 0, false);
                }
                else if ((data[23] & 1) == 1)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("9"), 0, false);
                }
                else if ((data[23] & 2) == 2)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("10"), 0, false);
                }
                else if ((data[23] & 4) == 4)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("11"), 0, false);
                }
                else if ((data[23] & 8) == 8)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("12"), 0, false);
                }
                else if ((data[23] & 16) == 16)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("13"), 0, false);
                }
                else if ((data[23] & 32) == 32)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("14"), 0, false);
                }
                else if ((data[23] & 64) == 64)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("15"), 0, false);
                }
                else if ((data[23] & 128) == 128)
                {
                    client.Publish($"/devices/Fan_{data[4]}/controls/AlarmCode", Encoding.UTF8.GetBytes("16"), 0, false);
                }
            }

            if (data[24] == 0 & data[25] == 0)
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Blok", Encoding.UTF8.GetBytes("0"), 0, false);
            }
            else
            {
                client.Publish($"/devices/Fan_{data[4]}/controls/Blok", Encoding.UTF8.GetBytes("1"), 0, false);
            }
        }
    }
}

Исходник mdvt к сожалению не нашел. Давно я это делал, ноутбук уде успел поменять. Но там нет ничего сложного. По тому коду который я скинул я думаю ты легко сможешь себе сам сделать тестер. Просто удалить все что связано с MQTT, и ответ от кондиционера выводить в консоль.

Спасибо, большое, если не сложно выложите, пожалуйста, архив вместе с всем проектом (sln и csproj), я с зависимостями тут запутался окончательно.

Еще вопрос, вы в результате успешно эксплуатируете это решение в быту?

Так же интересует ваше мнение, по поводу работы с портом не напрямую, а через “Прямое чтение и запись в порт”. Выглядит как будто это проще?

Не только. Он имеет RPC, который позволяет ставить в очередь обмена вообще произвольные наборы байт и получать на них ответ.
Пример реализации работы с произвольным протоколом можно посмотреть тут: Шаблон для электрокарниза - #6 от пользователя BrainRoot

Вот я как раз пример реализации и выложил. то есть - достаточно сформировать команду и потом распарсить ответ.

1 лайк

Архив на диске
У меня через эту программу подключено 20 устройств, собственно уже пол года работает без нареканий.

Спасибо, программу я видел выше, но она уже скомпелирована.
Если есть не просто один исходный файл, а весь проект, скиньте пожалуйста.

1 лайк

С исходниками разобрался, большое спасибо!

Я посмотрел много документации и, судя по ней, на внутренних блоках Midea VRF нет XYE, есть подозрение, что их переименовали в D1 D2 E, но это не точно). Как думаете,

  1. Можно ли подключиться к XYE наружного блока?
  2. Является ли D1 и D2 аналогами XY на внутреннем блоке?

Делюсь инфой, которую пока понял (возможно неправильно) по технической документации (основные источники тут и тут)

  1. PQE - шина (вроде RS485, но точно в документации про это не говорится) от наружного блока ко всем внутренним блокам. По этой шине Внешний блок общается со всеми Внутренними
  2. XYE - шина (вроде тоже RS485) от Внешнего блока к системе центрального управления, например CCM15 или CCM-180A или GW-MOD и тд
  3. D1 D2 - шина между внутренними блоками (вроде тоже RS485, но это не точно), к которой подключается групповой пульт управления, например, WDC-120G/WK или GW-KNX
  4. X1 X2 - шина на внутреннем блоке для проводного пульта управления конкретным блоком, по ней идет как сигнал, так и питание, не похоже что это RS485. К ней подключается пульт KJR-29B(1)/BK-E

PQE - шина RS485, которая используется для связи внешних и внутренних блоков. Протокол MODBUS. Здесь есть список переменных, но я его не проверял. https://support.wirenboard.com/uploads/short-url/22ZpkI6mQgSLjX2IPrvAGe6xLDE.pdf

XYE - шина RS485, эта шина используется для группового управления фанкойлами или кондиционерами, предназначена для пультов группового управления и шлюза для дистанционного управления.
D1 D2 - шина - такую шину не испытывал, судя по названию пульта GW-KNX, то смею предположить что это пульт который может подключатся по протоколу KNX.
X1 X2 это точно не RS485
Можно ли подключиться к XYE наружного блока? Теоретически да, но в моей программе нет настройки для наружного блока. Поэтому не рекомендую подключать.

Добрый день, сделал по вашей инструкции все, кондей Lessar канального типа по умолчанию сидит на 0 порте:
Imgur
Imgur
подкинул провода на отдельный свободный порт
Х - А
Y - B
E - GND
порт удалил и закачал файлы в /usr/bin/
mdv выдал разрешение на запуск

Imgur

настроил MDV.js правило

Imgur
Imgur

в списке девайсов появились вирт устройства кондиционера и статус опроса

Imgur

Однако “Кондиционер 0” имеет дефолтные значения мета-топиков, которые прописаны в MDV.js и данные судя по всему не получает.
Заметил, что есть настройка для установки режима работы MASTER-SLAVE на самом кондиционере:

Imgur

Установил переключатели из OFF_OFF в режим OFF_ON - кондиционер начал реагировать на подающие команды: включается диод при изменении POWER на значение 1 и гаснет при выключении (на панели), режимы тоже вроде бы переключает (физически чувствую или холод или тепло, по уровню потока воздуха тоже вроде бы изменяется корректно, однако никакие из этих изменений НЕ отображаются на панели. Когда на панели меняю это вручную - все тоже работает, однако изменений в вирт устройстве не вижу - код мета топика ALARM как был 2, так и остался. Заметил еще одну особенность: при нажатии физических клавиш на пульте управления происходит характерный звуковой сигнал, однако при изменении режимов работы и скорости вентилятора через вирт устройство - этих звуковых сигналов нет, но изменения происходят. Изменяя температуру - то же самое, т.е. на панели отображение не меняется, в ВБ не передается, мой высокоточный пальцометр не в состоянии определить меняется ли температура, но полагаю, что как и со скоростью воздушного потока - меняется.

То есть фактически управление есть в одностороннем порядке, но его как бы нет, т.к. нет обратной связи на панельке или отображении в вирт девайсе ВБ.
Подскажите, куда копать и в чем может быть дело?

Большое спасибо за проделанную вами работу и буду очень признателен за помощь.

Я недавно тоже подключал один кондиционер, и он тоже выдавал такие симптомы. Я ничего в итоге не сделал, просто пытался много раз подключиться, и кондей в итоге подключился нормально и работает. Тут следует отметить что на вход кондиционер слушает топики с окончанием /on. Чтобы изменить любой топик, к нему нужно обращаться из правил вот так - publish(“/devices/FanCoil/controls/SetTemp/on”, 22); или из любого другого устройства отправлять уставки на топик кондея с окончанием on. Что касается изменений данных на пульте управления, то тут не все так просто. Пульт не мониторит кондиционер, он лишь отправляет уставку. В каком режиме кондиционер в данный момент, пульт не знает. Это по сути тот же инфракрасный пульт, только он передает сигнал по проводу. А то что он не пищит, это тоже нормально. Так как в кондиционерах пищит именно та плата, которая принимает сигнал от инфракрасного пульта, в нее же включается и проводной пульт, а мы подключаемся через другой порт.
После любой настройки на плате кондея, его нужно перезапустить по питанию, причем выключать нужно минимум минут на 5.

1 лайк

Благодарю за ответ!
А для этого надо перекомпилировать MDV?
По поводу перенастройки на плате и перезагрузки по питанию - учту, проверю.

Зачем перепрограммировать? Это сейчас так работает. Если хочешь, то можешь и перепрограммировать.

1 лайк