Alle Beiträge von ingmarsretro

TV-Deckenhalterung motorisiert über Homeassistant steuern

Loading

Seit ich mich mit Home Automatisierungen beschäftige soll natürlich so viel wie möglich optimiert, vereinfacht und unter den Aspekten der neuen Schlagworte „Green Electronics“, „Nachhaltigkeit“, „Energiesparend“ … usw. angepasst und realisiert werden. So schalten bei mir Geräte bei Nichtbenutzung oder Nichtbeachtung ab, Stand-by Energieverbrauch wird weitgehend vermieden und auch die menschliche Vergesslichkeit (Fenster offen gelassen im Winter, oder vergessen Licht aus zu schalten) verhindert die IOT – Technologie. Wie die Leser des Blogs mittlerweile schon wissen habe ich hier Systeme wie HomeMatic, NodeRed und seit einiger Zeit Homeassistant mit ESPHome, Zigbee2Mqtt usw. in Verwendung. Das Ziel ist natürlich auch, alles Systeme Cloudfrei zu halten. Ich will nicht, dass die Daten den Umweg über irgendwelche Server in Fernost nehmen um bei mir ein Licht ein und aus zu schalten. Also soll möglichst alles im Kreis meines eigenen Netzwerks stattfinden und nicht nach außen „telefonieren“ und auch funktionieren wenn ich die Datenleitung kappe.

Bei diversen Lieferanten gibts es seit langem, ein, für die Bequemlichkeit im elterlichen Ruheraum, äußert praktisches Gerät. Ich spreche da von einer platzsparenden Möglichkeit, die Flimmerkiste (heute auch Flat-TV genannt) im Raum unterzubringen. Ich nenne hier nur Bezeichnungen wie:

Speaka Professional TV-Deckenhalterung elektrisch motorisiert (1439178) oder MyWall HL46ML … etc. Manche von diesen Gräten sind mit einer Funkfernbedienung steuerbar, andere wiederum über die CloudApp von Tuya. Man kann die Tuya App zwar über die Tuya IOT Entwicklungsumgebung umgehen und diese Geräte über die Integration „TuyaLocal“ in seinen Homeassistant bringen – geht zwar – ist aber eher eine „NUR“- Lösung. Die ideale Lösung ist aus meiner Sicht, die Integration dieser Geräte ins ESPHome System.  Am Beispiel der Speaka Professional TV Deckenhalterung zeige ich, wie diese mit einer kleinen Erweiterung ins ESPHome Netz und somit im Homeassistant integriert werden kann. Diese Ausführung des SpeaKa Teils hat keine Internet Anbindung und wird nur über eine Funkfernbedienung gesteuert.

TV Deckenhalter mit geöffneter Abdeckung

Mit ein wenig reverse Engineering haben wir (Kollege Werner und meiner einer) das bestehende Gerätewerk analysiert. Das System ist in etwa so aufgebaut:

Platine in der Deckenhalterung

 

Systemdiagramm

Das Systemdiagramm oben zeigt wie die Platine aufgebaut ist. Die Stromversorgung kommt von einem Steckernetzteil mit DC 24V Ausgang bei 1,5A. Auf der Platine erkennt man noch einen unbestückten Bereich, dessen Lötpads mit +3V3, GND und RX, TX Leitungen passend für einen ESP8266 beschaltet sind. Ebenso ist eine USB Buchse zu erkennen. Diese beiden Schnittstellen sind im Diagramm nicht berücksichtigt. Untersucht haben wir die RX/TX Leitungen, die von den unbestückten Lötpads (ESP8266) zum Microcontroller (1301 X 016B) geroutet sind. Doch hier waren keinerlei Signale zu messen. (Vermutlich ist die Schnittstelle in der geflashten Programmversion nicht aktiviert).

„Debug“ Drähte an den RX/TX und am RF-Chip

Dieser Weg bringt uns also nicht weiter. Im nächsten Schritt haben wir uns angesehen wo die Steuersignale der Funkfernbedienung herkommen, bzw. wie sie in weiterer Folge umgesetzt werden. Der RF-Empfänger Chip hat 16 Pins und leider keinerlei Beschriftung. Oder wurde sie entfernt. Die Versorgungsspannung des RF-Chips liegt an Pin1 und Pin16 an, Pin2 und Pin3 ist mit einem Quarz beschaltet und von Pin9 ist eine Leitung zum Microcontroller geroutet. Das muss also der Datenausgang sein. Mit Hilfe der Software „PulseView“ von Sigrok und einem Fernost Logicanalyzer haben wir diesen Ausgang mitgesnifft. Und siehe da, hier offenbarten sich Datenpakete mit einer Dauer von 10.3ms. Die Software PulseView konnte das Protokoll nach einigen Versuchen mit unterschiedlichen analysierten Datenraten als RS232 Protokoll erkennen. So war es dann ein leichtes die empfangenen und dekodierten Steuerbefehle zum Microcontroller zu protokollieren.

RF-Chip mit angeschlossener „Sniffer“ Leitung

Die Baudrate des RS232 Ports am RF-Chip Ausgangs beträgt 9600 bei 8N1. Es werden bei jedem gesendeten Befehl 10 Bytes in HEX empfangen. Hier die Liste der Kommandos: (fehlende Byte folgen…)

Befehl Byte0 Byte1 Byte2 Byte3 Byte4 Byte5 Byte6 Byte7 Byte8 Byte9
UP 0xAA 0x06 0x04 0x25 0x03 0xD5 0x01 0x00 0x02 0x55
DOWN 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x10 0x11 0x55
LEFT 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
RIGHT 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
BUTTON1 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
BUTTON2 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x08 0x09 0x55
MEM1 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
MEM2 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
OK 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x40 0x41 0x55
SET xx xx xx xx xx xx xx xx xx xx

Nachdem mit dem Logikanalyzer das Datenprotokoll gefunden war, versuchten wir über ein Terminalprogramm und einen USB zu TTL232 Converter die Daten an den Microcontroller zu senden. Dazu wurde der RF-Chip entfernt. Er zog den Pegel im Ruhezustand auf VCC und verhinderte ein paralleles Betreiben der „RS232 Transmitter“.

RF-Chip entfernt
Board ohne Chip mit Debugleitung

 

USB UART zum Senden der Befehle

Die Steuerbefehle aus oben dargestellter Tabelle konnten per Terminal Programm erfolgreich gesendet werden. Jetzt musste nur noch ein ESP32 Board diese Aufgabe übernehmen. Ein ESP32 NodeMCU Board aus dem Fundus wurde mit einem Basis ESPHome-Image bestückt und ins Homeassistant Netzwerk integriert. Dem ESPHome Knoten war jetzt nur noch beizubringen, über den TX Pin des ESP32 die Bytefolge bei entsprechendem Trigger im Homeassistant zu senden. Dazu wurde das ESP32 Board in auf der Platine befestigt und die VCC3V3, GND und TX Leitung zum PIN9 des ehemaligen RF Chip gelötet.

ESP32 am Board des Speaka Deckenhalters

 

Wieder im Deckenhalter eingebaut

In der ESPHome Webumgebung ist nun das folgende yaml Script hinzuzufügen.

 esphome:  
  name: tvhalterung  
  friendly_name: TVHalterung  
   
 esp32:  
  board: esp32dev  
  framework:  
   type: arduino  
   
 # Enable logging  
 logger:  
   
 # Enable Home Assistant API  
 api:  
  encryption:  
   key: "hier dein key beim Anlegen des device"  
   
 ota:  
  password: "hier dein ota password"  
   
 wifi:  
  ssid: !secret wifi_ssid  
  password: !secret wifi_password  
   
  # Enable fallback hotspot (captive portal) in case wifi connection fails  
  ap:  
   ssid: "Tvhalterung Fallback Hotspot"  
   password: "hier wieder deins"  
   
 captive_portal:  
   
 uart:  
  tx_pin: 4  
  rx_pin: 5  
  baud_rate: 9600  
   
 # Example button configuration  
 button:  
  - platform: template  
   name: TV Halterung UP  
   id: tv_up  
   icon: "mdi:arrow-up-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Up"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x01,0x00,0x02,0x55]  
    
  - platform: template  
   name: TV Halterung OK  
   id: tv_ok  
   icon: "mdi:stop-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV OK"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x40,0x41,0x55]  
   
  - platform: template  
   name: TV Halterung DOWN  
   id: tv_down  
   icon: "mdi:arrow-down-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Down"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x10,0x11,0x55]  
   
  - platform: template  
   name: TV Halterung Button1  
   id: tv_button1  
   icon: "mdi:numeric-1-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Button2  
   id: tv_button2  
   icon: "mdi:numeric-2-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x08,0x09,0x55]  
    
  - platform: template  
   name: TV Halterung Left  
   id: tv_left  
   icon: "mdi:arrow-left-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Left"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Right  
   id: tv_right  
   icon: "mdi:arrow-right-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Right"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
    
  - platform: template  
   name: TV Halterung MEM1  
   id: tv_mem1  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  
   
  - platform: template  
   name: TV Halterung MEM2  
   id: tv_mem2  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  

Ist das yaml Script dann kompiliert und zum ESP hochgeladen, gibt es ein neues ESPHome Device mit dem Namen TV-Halter in der Homeassistant Umgebung. Hier sind nun die Tasten für die Steuerung als Entitäten gelistet. Hat alles gekplappt, sollte sich die TV-Halterung über den Homeassistant jetzt steuern lassen.

(Es sind noch nicht alles Steuerkommandos richtig implementiert – die korrekten Codes werden in der Tabelle noch ergänzt)

Pylontech PV-Akkustatus im HomeAssistant

Loading

Wer eine Photovoltaik Anlage in seinem Eigenheim aufgebaut hat, verwendet vielleicht sogar einen Energiespeicher. In diesem Beispiel handelt es sich um eine Offgrid Anlage, die mit zwei Modulen des Herstellers Pylontech ausgestattet ist. Die Pylontech Akkus der Type US3000C haben eine Ausgangs Spannung von 48V. Die Nennkapazität beträgt 3500Wh. Die verbauten Zellen sind LiFePO4 Zellen und die nutzbare Kapazität ist lt. Datenblatt mit 3374Wh.angegeben. Die Akkus sind so gebaut, dass sie mit weiteren Pylontech-Akkus parallelgeschaltet werden können. Das intern verbaute BMS (BatterieManagementSystem) kommuniziert über eine sogenannte „Link“ Schnittstelle mit den anderen Pylontech Batteriemodulen. Ein als „Master“ konfigurierter Akku erledigt den Datenaustausch zum Wechselrichter. Hier stellt Pylontech den CAN- oder RS485 Bus als Schnittstelle zur Verfügung. Will man jedoch Informationen über die einzelnen Zellen (Spannungen, Ströme, Ladungen, Temperaturen etc.) haben, so gibt es an jedem Modul noch eine Schnittstelle mit der Bezeichnung „Console“. Das ist eine RS232 Schnittstelle über die man direkt mit dem BMS des Akkus kommunizieren kann. Dieser Port wird auch genutzt, um die Firmware des BMS zu aktualisieren. Ich rate aber DRINGEND davon ab, mit Firmwareupdates und Flashsoftware daran herum zu spielen. Das ist dem Hersteller oder dem Haftungsträger vorbehalten.

Da man über diese Schnittstelle aber auch einiges an Informationen über die im Akku verbauten Zellen erfährt, ist das ein interessanter Zugang. So hatte ich anfangs einen Laptop mit einem Terminal angeschlossen und konnte so die einzelnen Zellenspannungen und vor allem den evtl. unterschiedlichen Ladezustand der parallel geschalteten Module entdecken und monitoren. So dachte ich mir, wäre es doch eine gute Idee, wenn man diese Infos in seiner Hausautomatisierung zur Verfügung hat und dort visualisieren und für Steuerungszwecke nutzen kann.

Da für uns Freaks und Technikinteressierte Begriffe wie Homeassistant, Docker, Proxmox, HomeMatic, NodeRed usw. durchaus bekannt sind, dachte ich mir, diese Daten sollen auch im Homeassistant zu Entitäten werden. So war schnell wieder ein kleines neues Projekt geschaffen. Mein Plan: die Daten der Serien Schnittstelle auslesen und per MQTT an den Homeassistant senden.

Bevor ich mich nun aber mit dem Zerlegen der Datenstrings, die über den seriellen Port herauspurzeln beschäftige, bemühte ich zuerst einmal die Suchmaschinen. Vielleicht hat sich ja schon jemand anderes mit diesem Thema beschäftigt.  Und genau so war es auch. Auf GitHub wurde ich unter dem Begriff „pylontec2mqtt“ fündig. Unter https://github.com/irekzielinski/Pylontech-Battery-Monitoring ist ein Projekt gehostet, das mittels ESP8266 die seriellen Daten vom Port abholt und per MQTT und Wifi zum Homeassistant Server sendet. Ein Fork mit einer Weiterentwicklung dieses Projektes ist unter https://github.com/hidaba/PylontechMonitoring zu finden.

Warum ich das Projekt trotz des einfachen Nachbaus hier im Blog veröffentliche? Ich habe die Schaltung ein wenig optimiert und in ein Layout gepackt und den Code etwas angepasst. Das Ergebnis möchte ich hier teilen. Mir war es in wichtig, einen vernünftigen Aufbau auf einer Platine zu haben, die mit einem USB A-B Kabel für die Spannungsversorgung und einem LAN-RJ45 Kabel für die Datenverbindung angesteckt wird. Dabei wollte ich eine „solide“ USB Steckverbindung verwenden (nicht die Fragilen Mini- oder Mikro USB Steckverbindungen)

Auf einer Lochrasterplatine und mit den üblichen Entwicklungsplatinen habe ich mir auf die Schnelle ein Funktionsmuster „zusammen gestrickt“ um darauf die Software anpassen zu können.

Funktionsmuster auf Lochraster

So habe ich zuerst aus den Skizzen im Git-Projekt einen Schaltplan erstellt. An der „Console“ Schnittstelle liegt ein „echter“ RS232 Pegel an, der über den MAX3232 IC auf eine 5V TTL umgesetzt wird. Mit dem BSS123 FET wird für die Signale RX und TX je ein Pegelwandler auf 3.3V realisiert.

pylontec2mqtt schematic

Diese 3.3V TTL Pegel verarbeitet der ESP8266 in Form des Wemos D1Mini oder WemosD1Pro Entwicklungsboard, das auf die Platine aufgesteckt wird. Die gesamte Konstruktion habe ich dann in ein kleines Kunststoffgehäuse gepackt, das bequem über die Lan und USB Kabel mit dem Pylontec und einer USB Spannungsquelle zu verbinden ist.

Layout im Designtool

Im Bild oben ist der Layout Entwurf dargestellt. Die Platine und die Lage der Bauteile wurden vor der Fertigung nochmals mit der Vorschau überprüft und dann beim Hersteller des Vertrauens bestellt.

Vorschau der Platine vor der Fertigung

Nach kaum zwei Wochen Wartezeit hielt ich dann die Leeren Platinen in Händen und konnte sie mit den Bauteilen bestücken.

fertig bestückte Platine

Im Bild oben ist die fertig bestückte Platine zu sehen. Hier fehlt nur mehr das Wemosboard mit dem ESP.

Vergleich zwischen Funktionsmuster und erstem „Fertigungsmodell“

Schlussendlich habe ich einen WemosD1 Pro aufgesteckt, da dieser die Möglichkeit bietet, eine externe WiFi Antenne anzuschließen und somit eine vernünftige Funkreichweite zu erhalten.

Nach dem Aufspielen der Software und der Inbetriebnahme ist der Webserver des Wemos unter der im Code angegebenen IP Adresse erreichbar. Hier kann auch gleich geprüft werden, ob der Pylontech Akku mit dem Wemos kommuniziert. Das Ergebnis sieht dann wie folgt aus.

Webseite des WEMOS

Hier ist zu erkennen, dass beide Akkumodule korrekt erkannt werden. Im nächsten Schritt wird überprüft, ob über das MQTT Protokol Nachrichten versandt werden. Die IP Adresse des MQTT Brokers ist auch im Code des Wemos festzulegen. Ich habe in meinem Aufbau den MQTT-Explorer im Homeassistant eingerichtet, um schnell und einfach die MQTT Funktionen prüfen zu können.

MQTT Explorer

Im Bild oben ist zu erkennen, dass auch die Daten über MQTT korrekt ankommen. Jetzt ist es nur mehr notwendig, im Homeassistant ein Sensor yaml file anzulegen, um die topics im als Entitäten zur Verfügung zu stellen.  Dazu habe ich im configuration.yaml folgenden code hinzugefügt:

mqtt:
  sensor:
#Pylontec Akku Serial Readout (ESP32 192.168.xxx.yyy)
    - state_topic: "ingmarsretro/pylontec/ESP_WiFi_RSSI"
      name: "Pylontec_RSSI"
      unit_of_measurement: dB
      
    - state_topic: "ingmarsretro/pylontec/availability"
      name: "Pylontec_Status"
      
    - state_topic: "ingmarsretro/pylontec/currentDC"
      name: "DC-Strom"
      unit_of_measurement: "mA"
      
    - state_topic: "ingmarsretro/pylontec/getPowerDC"  
      name: "getPower DC"
      unit_of_measurement: "W"
      
    - state_topic: "ingmarsretro/pylontec/powerIN"  
      name: "Power IN"
      unit_of_measurement: "W"  
      
    - state_topic: "ingmarsretro/pylontec/estPowerAC"
      name: "Pylontec_estPowerAC"
      unit_of_measurement: Watt  
      
    - state_topic: "ingmarsretro/pylontec/soc"
      name: "Pylontec_SOC"
      unit_of_measurement: "%"  
      
    - state_topic: "ingmarsretro/pylontec/temp"
      name: "Pylontec_temperature"
      unit_of_measurement: "°C"
      
    - state_topic: "ingmarsretro/pylontec/battery_count"
      name: "Pylontec_BatteryCount"
      unit_of_measurement: "pcs"      
      
    - state_topic: "ingmarsretro/pylontec/base_state"
      name: "Pylontec_BaseState"
      
    - state_topic: "ingmarsretro/pylontec/is_normal"  
      name: "Pylontec_is_normal"

    - state_topic: "ingmarsretro/pylontec/powerOUT"
      name: "Pylontec_powerOUT"
      unit_of_measurement: Watt
      
# Pylontech battery module 0      
      
    - state_topic: "ingmarsretro/pylontec/0/current"
      name: "Pylontec_Battery0_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/0/voltage"
      name: "Pylontec_Battery0_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/0/soc"
      name: "Pylontec_Battery0_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/0/charging"
      name: "Pylontec_Battery0_charging"
      
    - state_topic: "ingmarsretro/pylontec/0/discharging"
      name: "Pylontec_Battery0_discharging"
     
    - state_topic: "ingmarsretro/pylontec/0/idle"
      name: "Pylontec_Battery0_idle"
      
    - state_topic: "ingmarsretro/pylontec/0/state"
      name: "Pylontec_Battery0_state"
      
    - state_topic: "ingmarsretro/pylontec/0/temp"
      name: "Pylontec_Battery0_temp"
      unit_of_measurement: "°C"
      
# Pylontech battery module 1      
      
    - state_topic: "ingmarsretro/pylontec/1/current"
      name: "Pylontec_Battery1_current"
      unit_of_measurement: "A"

    - state_topic: "ingmarsretro/pylontec/1/voltage"
      name: "Pylontec_Battery1_voltage"
      unit_of_measurement: "V"
      
    - state_topic: "ingmarsretro/pylontec/1/soc"
      name: "Pylontec_Battery1_soc"
      unit_of_measurement: "%"
      
    - state_topic: "ingmarsretro/pylontec/1/charging"
      name: "Pylontec_Battery1_charging"
      
    - state_topic: "ingmarsretro/pylontec/1/discharging"
      name: "Pylontec_Battery1_discharging"
     
    - state_topic: "ingmarsretro/pylontec/1/idle"
      name: "Pylontec_Battery1_idle"
      
    - state_topic: "ingmarsretro/pylontec/1/state"
      name: "Pylontec_Battery1_state"
      
    - state_topic: "ingmarsretro/pylontec/1/temp"
      name: "Pylontec_Battery1_temp"
      unit_of_measurement: "°C"
      

Auf der Homeassistant Website könnte die Visualisierung dann beispielsweise so aussehen:

Zu guter Letzt poste ich unten noch den angepassten Code. Die zum Kompilieren notwendigen Libraries und weiteren Infos sind den oben angeführten GitHub Links zu entnehmen.

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <ESP8266WebServer.h>
#include <circular_log.h>
#include <ArduinoJson.h>
#include <NTPClient.h>
#include <ESP8266TimerInterrupt.h>

//+++ START CONFIGURATION +++

//IMPORTANT: Specify your WIFI settings:
#define WIFI_SSID "wifiname"
#define WIFI_PASS deinpasswort1234"
#define WIFI_HOSTNAME "mppsolar-pylontec"

//Uncomment for static ip configuration
#define STATIC_IP
  IPAddress local_IP(192, 168, xxx, yyy);
  IPAddress subnet(255, 255, 255, 0);
  IPAddress gateway(192, 168, xxx, zzz);
  IPAddress primaryDNS(192, 168, xxx, zzz);

//Uncomment for authentication page
//#define AUTHENTICATION
//set http Authentication
const char* www_username = "admin";
const char* www_password = "password";


//IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):
#define ENABLE_MQTT

// Set offset time in seconds to adjust for your timezone, for example:
// GMT +1 = 3600
// GMT +1 = 7200
// GMT +8 = 28800
// GMT -1 = -3600
// GMT 0 = 0
#define GMT 3600

//NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.
//NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"
#define MQTT_SERVER        "192.168.xx.broker"
#define MQTT_PORT          1883
#define MQTT_USER          ""
#define MQTT_PASSWORD      ""
#define MQTT_TOPIC_ROOT    "ingmarsretro/pylontec/"  //this is where mqtt data will be pushed
#define MQTT_PUSH_FREQ_SEC 2  //maximum mqtt update frequency in seconds

//+++   END CONFIGURATION +++

#ifdef ENABLE_MQTT
#include <PubSubClient.h>
WiFiClient espClient;
PubSubClient mqttClient(espClient);
#endif //ENABLE_MQTT

//text response
char g_szRecvBuff[7000];

const long utcOffsetInSeconds = GMT;
char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);


ESP8266WebServer server(80);
circular_log<7000> g_log;
bool ntpTimeReceived = false;
int g_baudRate = 0;

void Log(const char* msg)
{
  g_log.Log(msg);
}

//Define Interrupt Timer to Calculate Power meter every second (kWh)
#define USING_TIM_DIV1 true                                             // for shortest and most accurate timer
ESP8266Timer ITimer;
bool setInterval(unsigned long interval, timer_callback callback);      // interval (in microseconds)
#define TIMER_INTERVAL_MS 1000

//Global Variables for the Power Meter - accessible from the calculating interrupt und from main
unsigned long powerIN = 0;       //WS gone in to the BAttery
unsigned long powerOUT = 0;      //WS gone out of the Battery
//Global Variables for the Power Meter - Überlauf
unsigned long powerINWh = 0;       //WS gone in to the BAttery
unsigned long powerOUTWh = 0;      //WS gone out of the Battery

void setup() {
  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); //clean variable
  
  pinMode(LED_BUILTIN, OUTPUT); 
  digitalWrite(LED_BUILTIN, HIGH);//high is off
  
  // put your setup code here, to run once:
  WiFi.mode(WIFI_STA);
  WiFi.persistent(false); //our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)
  WiFi.hostname(WIFI_HOSTNAME);
  #ifdef STATIC_IP
     WiFi.config(local_IP, gateway, subnet, primaryDNS);
  #endif
  WiFi.begin(WIFI_SSID, WIFI_PASS);

  for(int ix=0; ix<10; ix++)
  {
    Log("Wait for WIFI Connection");
    if(WiFi.status() == WL_CONNECTED)
    {
      break;
    }

    delay(1000);
  }

  ArduinoOTA.setHostname(WIFI_HOSTNAME);
  ArduinoOTA.begin();
  server.on("/", handleRoot);
  server.on("/log", handleLog);
  server.on("/req", handleReq);
  server.on("/jsonOut", handleJsonOut);
  server.on("/reboot", [](){
    #ifdef AUTHENTICATION
    if (!server.authenticate(www_username, www_password)) {
      return server.requestAuthentication();
    }
    #endif
    ESP.restart();
  });
  
  server.begin(); 
  
  timeClient.begin();

  
#ifdef ENABLE_MQTT
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
#endif

  Log("Boot event");
  
}

void handleLog()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  } 
  #endif
  server.send(200, "text/html", g_log.c_str());
}

void switchBaud(int newRate)
{
  if(g_baudRate == newRate)
  {
    return;
  }
  
  if(g_baudRate != 0)
  {
    Serial.flush();
    delay(20);
    Serial.end();
    delay(20);
  }

  char szMsg[50];
  snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);
  Log(szMsg);
  
  Serial.begin(newRate);
  g_baudRate = newRate;

  delay(20);
}

void waitForSerial()
{
  for(int ix=0; ix<150;ix++)
  {
    if(Serial.available()) break;
    delay(10);
  }
}

int readFromSerial()
{
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));
  int recvBuffLen = 0;
  bool foundTerminator = true;
  
  waitForSerial();
  
  while(Serial.available())
  {
    char szResponse[256] = "";
    const int readNow = Serial.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)
    if(readNow > 0 && 
       szResponse[0] != '\0')
    {
      if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))
      {
        Log("WARNING: Read too much data on the console!");
        break;
      }
      
      strcat(g_szRecvBuff, szResponse);
      recvBuffLen += readNow;

      if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))
      {
        strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add
        foundTerminator = true;
        break; //found end of the string
      }

      if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))
      {
        //we need to send new line character so battery continues the output
        Serial.write("\r");
      }

      waitForSerial();
    }
  }

  if(recvBuffLen > 0 )
  {
    if(foundTerminator == false)
    {
      Log("Failed to find pylon> terminator");
    }
  }

  return recvBuffLen;
}

bool readFromSerialAndSendResponse()
{
  const int recvBuffLen = readFromSerial();
  if(recvBuffLen > 0)
  {
    server.sendContent(g_szRecvBuff);
    return true;
  }

  return false;
}

bool sendCommandAndReadSerialResponse(const char* pszCommand)
{
  switchBaud(115200);

  if(pszCommand[0] != '\0')
  {
    Serial.write(pszCommand);
  }
  Serial.write("\n");

  const int recvBuffLen = readFromSerial();
  if(recvBuffLen > 0)
  {
    return true;
  }

  //wake up console and try again:
  wakeUpConsole();

  if(pszCommand[0] != '\0')
  {
    Serial.write(pszCommand);
  }
  Serial.write("\n");

  return readFromSerial() > 0;
}

void handleReq()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  bool respOK;
  if(server.hasArg("code") == false)
  {
    respOK = sendCommandAndReadSerialResponse("");
  }
  else
  {
    respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());
  }

  handleRoot();
}



void handleJsonOut()
{
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  if(sendCommandAndReadSerialResponse("pwr") == false)
  {
    server.send(500, "text/plain", "Failed to get response to 'pwr' command");
    return;
  }

  parsePwrResponse(g_szRecvBuff);
  prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));
  server.send(200, "application/json", g_szRecvBuff);
}

void handleRoot() {
  #ifdef AUTHENTICATION
  if (!server.authenticate(www_username, www_password)) {
    return server.requestAuthentication();
  }
  #endif
  timeClient.update(); //get ntp datetime
  unsigned long days = 0, hours = 0, minutes = 0;
  unsigned long val = os_getCurrentTimeSec();
  days = val / (3600*24);
  val -= days * (3600*24);
  hours = val / 3600;
  val -= hours * 3600;
  minutes = val / 60;
  val -= minutes*60;

  time_t epochTime = timeClient.getEpochTime();
  String formattedTime = timeClient.getFormattedTime();
  //Get a time structure
  struct tm *ptm = gmtime ((time_t *)&epochTime); 
  int currentMonth = ptm->tm_mon+1;

  static char szTmp[9500] = "";  
  long timezone= GMT / 3600;
  snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Pylontech Battery</b><br>Time GMT: %s (%s %d)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>Wifi RSSI: %d<BR>Wifi SSID: %s", 
            formattedTime, "GMT ", timezone,
            (int)days, (int)hours, (int)minutes, (int)val, 
            ESP.getFreeHeap(), WiFi.RSSI(), WiFi.SSID().c_str());

  strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);
  strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'> <a href='/req?code=pwr'>PWR</a> | <a href='/req?code=pwr%201'>Power 1</a> |  <a href='/req?code=pwr%202'>Power 2</a> | <a href='/req?code=pwr%203'>Power 3</a> | <a href='/req?code=pwr%204'>Power 4</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
  //strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);
  strncat(szTmp, "<textarea rows='80' cols='180'>", sizeof(szTmp)-1);
  //strncat(szTmp, "<textarea rows='45' cols='180'>", sizeof(szTmp)-1);
  strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);
  strncat(szTmp, "</textarea></form>", sizeof(szTmp)-1);
  strncat(szTmp, "</html>", sizeof(szTmp)-1);
  //send page
  server.send(200, "text/html", szTmp);
}

unsigned long os_getCurrentTimeSec()
{
  static unsigned int wrapCnt = 0;
  static unsigned long lastVal = 0;
  unsigned long currentVal = millis();

  if(currentVal < lastVal)
  {
    wrapCnt++;
  }

  lastVal = currentVal;
  unsigned long seconds = currentVal/1000;
  
  //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter
  return (wrapCnt*4294967) + seconds;
}

void wakeUpConsole()
{
  switchBaud(1200);

  //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};
  //Serial.write(wakeUpBuff, sizeof(wakeUpBuff));
  Serial.write("~20014682C0048520FCC3\r");
  delay(1000);

  byte newLineBuff[] = {0x0E, 0x0A};
  switchBaud(115200);
  
  for(int ix=0; ix<10; ix++)
  {
    Serial.write(newLineBuff, sizeof(newLineBuff));
    delay(1000);

    if(Serial.available())
    {
      while(Serial.available())
      {
        Serial.read();
      }
      
      break;
    }
  }
}

#define MAX_PYLON_BATTERIES 8

struct pylonBattery
{
  bool isPresent;
  long  soc;     //Coulomb in %
  long  voltage; //in mW
  long  current; //in mA, negative value is discharge
  long  tempr;   //temp of case or BMS?
  long  cellTempLow;
  long  cellTempHigh;
  long  cellVoltLow;
  long  cellVoltHigh;
  char baseState[9];    //Charge | Dischg | Idle
  char voltageState[9]; //Normal
  char currentState[9]; //Normal
  char tempState[9];    //Normal
  char time[20];        //2019-06-08 04:00:29
  char b_v_st[9];       //Normal  (battery voltage?)
  char b_t_st[9];       //Normal  (battery temperature?)

  bool isCharging()    const { return strcmp(baseState, "Charge")   == 0; }
  bool isDischarging() const { return strcmp(baseState, "Dischg")   == 0; }
  bool isIdle()        const { return strcmp(baseState, "Idle")     == 0; }
  bool isBalancing()   const { return strcmp(baseState, "Balance")  == 0; }
  

  bool isNormal() const
  {
    if(isCharging()    == false &&
       isDischarging() == false &&
       isIdle()        == false &&
       isBalancing()   == false)
    {
      return false; //base state looks wrong!
    }

    return  strcmp(voltageState, "Normal") == 0 &&
            strcmp(currentState, "Normal") == 0 &&
            strcmp(tempState,    "Normal") == 0 &&
            strcmp(b_v_st,       "Normal") == 0 &&
            strcmp(b_t_st,       "Normal") == 0 ;
  }
};

struct batteryStack
{
  int batteryCount;
  int soc;  //in %, if charging: average SOC, otherwise: lowest SOC
  int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest
  long currentDC;    //mAh current going in or out of the battery
  long avgVoltage;    //in mV
  char baseState[9];  //Charge | Dischg | Idle | Balance | Alarm!

  
  pylonBattery batts[MAX_PYLON_BATTERIES];

  bool isNormal() const
  {
    for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
    {
      if(batts[ix].isPresent && 
         batts[ix].isNormal() == false)
      {
        return false;
      }
    }

    return true;
  }

  //in Wh
  long getPowerDC() const
  {
    return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
  }

  // power in Wh in charge
  float powerIN() const
  {
    if (currentDC > 0) {
       return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));
    } else {
       return (float)(0);
    }
  }
  
  // power in Wh in discharge
  float powerOUT() const
  {
    if (currentDC < 0) {
       return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0)*-1);
    } else {
       return (float)(0);
    }
  }

  //Wh estimated current on AC side (taking into account Sofar ME3000SP losses)
  long getEstPowerAc() const
  {
    double powerDC = (double)getPowerDC();
    if(powerDC == 0)
    {
      return 0;
    }
    else if(powerDC < 0)
    {
      //we are discharging, on AC side we will see less power due to losses
      if(powerDC < -1000)
      {
        return (long)(powerDC*0.94);
      }
      else if(powerDC < -600)
      {
        return (long)(powerDC*0.90);
      }
      else
      {
        return (long)(powerDC*0.87);
      }
    }
    else
    {
      //we are charging, on AC side we will have more power due to losses
      if(powerDC > 1000)
      {
        return (long)(powerDC*1.06);
      }
      else if(powerDC > 600)
      {
        return (long)(powerDC*1.1);
      }
      else
      {
        return (long)(powerDC*1.13);
      }
    }
  }
};

batteryStack g_stack;


long extractInt(const char* pStr, int pos)
{
  return atol(pStr+pos);
}

void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)
{
  strOut[strOutSize-1] = '\0';
  strncpy(strOut, pStr+pos, strOutSize-1);
  strOutSize--;
  
  
  //trim right
  while(strOutSize > 0)
  {
    if(isspace(strOut[strOutSize-1]))
    {
      strOut[strOutSize-1] = '\0';
    }
    else
    {
      break;
    }

    strOutSize--;
  }
}

/* Output has mixed \r and \r\n
pwr

@

Power Volt   Curr   Tempr  Tlow   Thigh  Vlow   Vhigh  Base.St  Volt.St  Curr.St  Temp.St  Coulomb  Time                 B.V.St   B.T.St  

1     49735  -1440  22000  19000  19000  3315   3317   Dischg   Normal   Normal   Normal   93%      2019-06-08 04:00:30  Normal   Normal  

....   

8     -      -      -      -      -      -      -      Absent   -        -        -        -        -                    -        -       

Command completed successfully

$$

pylon
*/
bool parsePwrResponse(const char* pStr)
{
  if(strstr(pStr, "Command completed successfully") == NULL)
  {
    return false;
  }
  
  int chargeCnt    = 0;
  int dischargeCnt = 0;
  int idleCnt      = 0;
  int alarmCnt     = 0;
  int socAvg       = 0;
  int socLow       = 0;
  int tempHigh     = 0;
  int tempLow      = 0;

  memset(&g_stack, 0, sizeof(g_stack));

  for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)
  {
    char szToFind[32] = "";
    snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d     ", ix+1);

    const char* pLineStart = strstr(pStr, szToFind);
    if(pLineStart == NULL)
    {
      return false;
    }

    pLineStart += 3; //move past \r\r\n

    extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));
    if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)
    {
      g_stack.batts[ix].isPresent = false;
    }
    else
    {
      g_stack.batts[ix].isPresent = true;
      extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));
      extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));
      extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));
      extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));
      extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));
      extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));
      g_stack.batts[ix].voltage = extractInt(pLineStart, 6);
      g_stack.batts[ix].current = extractInt(pLineStart, 13);
      g_stack.batts[ix].tempr   = extractInt(pLineStart, 20);
      g_stack.batts[ix].cellTempLow    = extractInt(pLineStart, 27);
      g_stack.batts[ix].cellTempHigh   = extractInt(pLineStart, 34);
      g_stack.batts[ix].cellVoltLow    = extractInt(pLineStart, 41);
      g_stack.batts[ix].cellVoltHigh   = extractInt(pLineStart, 48);
      g_stack.batts[ix].soc            = extractInt(pLineStart, 91);

      //////////////////////////////// Post-process ////////////////////////
      g_stack.batteryCount++;
      g_stack.currentDC += g_stack.batts[ix].current;
      g_stack.avgVoltage += g_stack.batts[ix].voltage;
      socAvg += g_stack.batts[ix].soc;

      if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }
      else if(g_stack.batts[ix].isCharging()){chargeCnt++;}
      else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}
      else if(g_stack.batts[ix].isIdle()){idleCnt++;}
      else{ alarmCnt++; } //should not really happen!

      if(g_stack.batteryCount == 1)
      {
        socLow = g_stack.batts[ix].soc;
        tempLow  = g_stack.batts[ix].cellTempLow;
        tempHigh = g_stack.batts[ix].cellTempHigh;
      }
      else
      {
        if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}
        if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}
        if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}
      }
      
    }
  }

  //now update stack state:
  g_stack.avgVoltage /= g_stack.batteryCount;
  g_stack.soc = socLow;

  if(tempHigh > 15000) //15C
  {
    g_stack.temp = tempHigh; //in the summer we highlight the warmest cell
  }
  else
  {
    g_stack.temp = tempLow; //in the winter we focus on coldest cell
  }

  if(alarmCnt > 0)
  {
    strcpy(g_stack.baseState, "Alarm!");
  }
  else if(chargeCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Charge");
    g_stack.soc = (int)(socAvg / g_stack.batteryCount);
  }
  else if(dischargeCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Dischg");
  }
  else if(idleCnt == g_stack.batteryCount)
  {
    strcpy(g_stack.baseState, "Idle");
  }
  else
  {
    strcpy(g_stack.baseState, "Balance");
  }


  return true;
}

void prepareJsonOutput(char* pBuff, int buffSize)
{
  memset(pBuff, 0, buffSize);
  snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc, 
                                                                                                                                                                                                            g_stack.temp, 
                                                                                                                                                                                                            g_stack.currentDC, 
                                                                                                                                                                                                            g_stack.avgVoltage, 
                                                                                                                                                                                                            g_stack.baseState, 
                                                                                                                                                                                                            g_stack.batteryCount, 
                                                                                                                                                                                                            g_stack.getPowerDC(), 
                                                                                                                                                                                                            g_stack.getEstPowerAc(),
                                                                                                                                                                                                            g_stack.isNormal() ? "true" : "false");
}

void loop() {
#ifdef ENABLE_MQTT
  mqttLoop();
#endif
  
  ArduinoOTA.handle();
  server.handleClient();

  //if there are bytes availbe on serial here - it's unexpected
  //when we send a command to battery, we read whole response
  //if we get anything here anyways - we will log it
  int bytesAv = Serial.available();
  if(bytesAv > 0)
  {
    if(bytesAv > 63)
    {
      bytesAv = 63;
    }
    
    char buff[64+4] = "RCV:";
    if(Serial.readBytes(buff+4, bytesAv) > 0)
    {
      digitalWrite(LED_BUILTIN, LOW);
      delay(5);
      digitalWrite(LED_BUILTIN, HIGH);//high is off

      Log(buff);
    }
  }
}

#ifdef ENABLE_MQTT
#define ABS_DIFF(a, b) (a > b ? a-b : b-a)
void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)
{
  char szTmp[16] = "";
  snprintf(szTmp, 15, "%.2f", newValue);
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  {
    mqttClient.publish(topic, szTmp, false);
  }
}

void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)
{
  char szTmp[16] = "";
  snprintf(szTmp, 15, "%d", newValue);
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)
  {
    mqttClient.publish(topic, szTmp, false);
  }
}

void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)
{
  if(force || strcmp(newValue, oldValue) != 0)
  {
    mqttClient.publish(topic, newValue, false);
  }
}

void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)
{
  mqtt_publish_f(MQTT_TOPIC_ROOT "soc",          g_stack.soc,                lastSentData.soc,                0, forceUpdate);
  mqtt_publish_f(MQTT_TOPIC_ROOT "temp",         (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0.1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "currentDC",    g_stack.currentDC,          lastSentData.currentDC,          1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC",   g_stack.getEstPowerAc(),    lastSentData.getEstPowerAc(),   10, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount,       lastSentData.batteryCount,       0, forceUpdate);
  mqtt_publish_s(MQTT_TOPIC_ROOT "base_state",   g_stack.baseState,          lastSentData.baseState            , forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal",    g_stack.isNormal() ? 1:0,   lastSentData.isNormal() ? 1:0,   0, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "getPowerDC",   g_stack.getPowerDC(),       lastSentData.getPowerDC(),       1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerIN",      g_stack.powerIN(),          lastSentData.powerIN(),          1, forceUpdate);
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerOUT",     g_stack.powerOUT(),         lastSentData.powerOUT(),         1, forceUpdate);

  // publishing details
  for (int ix = 0; ix < g_stack.batteryCount; ix++) {
    char ixBuff[50];
    String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/voltage";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage / 1000.0, lastSentData.batts[ix].voltage / 1000.0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/current";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, g_stack.batts[ix].current / 1000.0, lastSentData.batts[ix].current / 1000.0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/soc";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/charging";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/discharging";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/idle";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/state";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_s(ixBuff, g_stack.batts[ix].isIdle()?"Idle":g_stack.batts[ix].isCharging()?"Charging":g_stack.batts[ix].isDischarging()?"Discharging":"", lastSentData.batts[ix].isIdle()?"Idle":lastSentData.batts[ix].isCharging()?"Charging":lastSentData.batts[ix].isDischarging()?"Discharging":"", forceUpdate);
    ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/temp";
    ixBattStr.toCharArray(ixBuff, 50);
    mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr/1000.0, (float)lastSentData.batts[ix].tempr/1000.0, 0.1, forceUpdate);
  }
} 

void mqttLoop()
{
  //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)
  static unsigned long g_lastConnectionAttempt = 0;

  //first: let's make sure we are connected to mqtt
  const char* topicLastWill = MQTT_TOPIC_ROOT "availability";
  if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {
    if(mqttClient.connect(WIFI_HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))
    {
      Log("Connected to MQTT server: " MQTT_SERVER);
      mqttClient.publish(topicLastWill, "online", true);
    }
    else
    {
      Log("Failed to connect to MQTT server.");
    }

    g_lastConnectionAttempt = os_getCurrentTimeSec();
  }

  //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)
  static unsigned long g_lastDataSent = 0;
  if(mqttClient.connected() && 
     os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&
     sendCommandAndReadSerialResponse("pwr") == true)
  {
    static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again
    static unsigned int callCnt = 0;
    
    parsePwrResponse(g_szRecvBuff);

    bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call
    pushBatteryDataToMqtt(lastSentData, forceUpdate);
    
    callCnt++;
    g_lastDataSent = os_getCurrentTimeSec();
    memcpy(&lastSentData, &g_stack, sizeof(batteryStack));
  }
  
  mqttClient.loop();
}

#endif //ENABLE_MQTT

 

Dyson – es muss nicht immer der Akku sein

Loading

Immer wieder werde ich angesprochen, mir fehlerbehaftete, kabellose Staubsauggeräte anzusehen. Dabei sind die modernen Geräte nach dem Zyklon Prinzip am häufigsten vertreten. Meist sind es die Akkus, deren BMS (Batterie-Management-System) dem Akku den „Garaus“ macht, indem der im BMS verbaute Microcontroller ein „disabled“ Flag setzt und den Akku unbrauchbar macht. Der Grund dafür ist meist ein defekt einer oder mehrerer Zellen. Genauer gesagt ist es die Balance der Zellen, die nicht mehr gegeben ist oder erreicht wird. Bei vielen Akkusystemen moderner kabelloser Geräte kommen Lithium-Ionen Zellen der Baugröße 18650 zum Einsatz. (zylindrische Zellen mit 18mm Durchmesser und 65mm Länge, einer Nominalspannung von 3.7V und einer Kapazität von durchschnittlich zwei Ah) Ein defekter oder ein, durch das BMS abgeschalteter Akku, lässt sich dann nicht mehr Laden, der Handstaubsauger schaltet nicht mehr ein, wenn der der Trigger gedrückt wird und es gibt je nach Bauart und Modell Blinksignale der im Akku verbauten Leuchtdioden. Zu diesen Fehlerbildern gibt es einige Ansätze, erfolgreiche Reparaturen durchzuführen. Zum einen können aufgrund des Alters des Gerätes Zellen defekt sein, die dann eine viel zu geringe oder gar keine Zellspannung mehr abgeben. Hier ist ein Tausch der einzelnen, betroffenen Zellen dann möglich – wenn auch nicht sinnvoll. Man kann natürlich auch alle Zellen erneuern – so man sich die Arbeit und den Umgang mit dem Punktschweißgerät und dem Nickelband zutraut. Ich hatte aber auch schon einige Geräte am Tisch, dessen Akkus auch keine Ausgangsspannung mehr freischalteten, obwohl die einzelnen Zellen und deren Leerlaufspannung absolut in Ordnung und die Balance untereinander gegeben war.  Der einfachste Lösungsansatz war dabei eine genauere Untersuchung der BMS Platine. Ein einfacher Reset des verbauten Microcontrollers erweckte den Akku schon häufig wieder zum Leben. (Hier ist natürlich vorausgesetzt, dass man am Board ein wenig Reverse Engineering betreibt und sich zumindest die Type des Microcontrollers und dessen Beschaltung herauszeichnet) Bei Recherchen im Netz bin ich sogar auf ein GitHub Projekt gestoßen, bei dem jemand eine eigene Firmware für das BMS Board einiger Dyson Akkus entwickelt hat.

Wie im Titel des Beitrags beschrieben, muss es aber nicht immer an einem defekten Akku liegen, dass der kleine Zyklon nicht mehr tobt. In diesem Fall hat sich folgendes Verhalten gezeigt:

Beim Betätigen des Triggers blinkt die LED am Dyson DC34 und der Motor dreht nicht.  Steckt man das Ladegerät an den Akku des Saugers an, so leuchtet die LED am Netzteil und es sieht alles nach einem, wie in der Bedienungsleitung beschriebenen Ladevorgang aus.

Dyson DC34 am Ladegerät – noch ist die LED an

Soweit so gut. Doch schon nach einigen Minuten erlischt die LED am Ladegerät und man bekommt signalisiert, dass der Akku vollgeladen ist. Dem ist aber nicht so. Wenn der Trigger betätigt wird, so passiert nichts außer, dass die LED am Dyson zu blinken beginnt. Der Motor bleibt still. Also war mein erster Verdacht – wie so oft – der Akku ist defekt. Also habe ich zuerst einmal den Akku demontiert und die Spannungen der 18650er Zellen gemessen. Die waren jedoch alle ok – also einem Bereich um die 3.5V und vor allem gab es auch unter den Zellen keine großen Abweichungen. Sie waren somit auch gut balanciert, aber eben leer. Im nächsten Schritt habe ich dann das BMS Board genauer betrachtet und den darauf befindlichen Mikrocontroller resettet. Ein erneuter Ladeversuch brachte aber keine Änderungen und LED am Ladegerät erlosch wieder nach einigen Minuten.

So war der nächste Schritt, den Akku mit einem Labornetzteil zu laden und dann messen wie sich das BMS und die Zellen verhalten. Das Modell DC34 ist eines der „älteren“ Geräte. Hier wurden auf der DC Seite noch zwei Spannungen benötigt. 16.75V DC und 24.35V DC werden über den dreipoligen Ladestecker geliefert. Da der Akku ja schon geöffnet war, war es ein Leichtes, die beiden Spannungen anzulegen. Und siehe da, das Laden der Zellen klappte. Nach einigen Minuten am Labornetzteil hatte der Dyson Akku wieder genug Energie um den Sauger zu betreiben. Somit konnte ich den Akku als Defekt ausschließen.

Der nächste Ansatz war nun, das Ladegerät zu untersuchen. Und um das Ergebnis vorweg zu nehmen – genau das war das Problem. Um hier die Ursache zu finden, entschloss ich mich, das Ladegerät zu öffnen. Aufgrund der Nachhaltigkeit der heutigen Konsumentenprodukte ist das Teil ganz einfach zu öffnen – lediglich zwei Kreuzschlitzschrauben halten die Gehäuseschalen zusammen und nach dem Lösen dieser, fallen die Teile einfach auseinander und die Platine liegt auf dem Tisch… – schön wär´s. Leider ist kein Hersteller daran interessiert, dass seine defekten Teile einfach zu reparieren sind. So ist das Gehäuse auch nicht verschraubt. Denn Schrauben sind ja auch teuer. Die Kunststoffteile sind natürlich verschweißt bzw. verklebt und nur mit einer Trennscheibe am Dremel zu lösen. Gesagt – getan.

Nach dem Öffnen und Freilegen der Netzteilplatine habe ich die DC-Ausgänge mit Lasten, die der aufgedruckten Nominalleistung entsprechen, belastet und gleichzeitig die Spannungen gemessen. Diese fallen unter Last um ein paar Millivolt ab, aber sind zu Anfang auch vorhanden. Dann passierte aber folgendes: Nach ein paar Minuten Betrieb fielen die Spannungen auf 0V ab und die LED leuchtete nicht mehr. Aha! Also kurz von der Netzspannung getrennt und wieder eingesteckt. Doch die LED blieb dunkel und es gab keine Ausgangsspannung. Erst nach einigen Minuten ohne AC-Versorgung fuhr das „Schaltnetzteil“ wieder hoch. Unter Last ließ sich das Verhalten immer nachvollziehen. Zwei bis fünf Minuten unter Last und das Netzteil schaltet ab. Bei einem Versuch ohne Ausgangsbelastung passierte lange nichts und die Spannungen standen stabil. Erst geschätzte dreißig Minuten später war wieder Ende. Nach einer genaueren Untersuchung ist mir dann aufgefallen, dass die primäre Ansteuerung des Wandlers auf einmal nicht mehr vorhanden war. Der Enable/Undervoltage Pin des Controller IC wurde aber im Ausschaltmoment über den Optokoppler Transistor nicht aktiviert und trotzdem war Schluss.

DC34 Ladegerät mit Lastwiderständen

Ein kurzes Berühren des Controller ICs mit dem Finger und das darauffolgende Wahrnehmen von unangenehmer Hitze und Schmerz eröffnete mir eine neue Fehlerursache. Das Teil wird thermisch zu heiß und es könnte ja eine „thermal-Protection“ geben. Bingo. Bei dem Controller IC handelt es sich um einen TNY278PN der TinySwitch Family von „power integrations“, einem IC der den Leistungs Mosfet und die Ansteuerelektronik (den PWM Generator) in einem Chip vereint hat. Und sieht man sich das Datenblatt an, dann ist folgendes zu lesen:

Auszug aus dem Datenblatt des TNY

Somit hat es etwas mit der Erwärmung des IC´s, bzw. dessen thermischer Belastung zu tun, die sich anscheinend nicht mehr im Sollbereich bewegt. Glücklicherweise schaltet der Chip durch seine eingebaute Sicherheitstechnik ab und nicht die Sicherung, die auslöst, wenn der Mosfet stirbt und dauerhaft niederohmig wird. Das Abschalten des Controller ICs auch bei Nichtbelastung war ein weiteres Indiz, dass der Chip nicht mehr in Ordnung ist. Also habe ich den IC erneuert und das Board wieder in Betrieb genommen. Die LED und die Ausgangsspannungen waren sofort wieder da. Nach 20 Minuten Dauerbetrieb mit angeschlossenen Lastwiderständen war immer noch alles OK. Auch die Temperatur des Controller IC war durchaus im erträglichen Bereich.

alt vs. neu

Also konnte ich davon ausgehen, dass das Ladegerät wieder ok war. Vor dem Verkleben der Gehäuseschalen bekam ein Gehäuseteil noch ein Tuning in Form von vier Lüftungslöchern. So gibt es zumindest die Möglichkeit, die thermische Abwärme im Netzteil abzuführen.

Netzteil mit Tuning 😀

 

 

 

Keysight DSO-X 2012A Oszilloskop stirbt im Standby – Netzteiltausch

Loading

In einem alten Beitrag aus dem Jahr 2019 habe ich über Oszilloskope des Herstellers Keysight und deren Problem mit einem plötzlichen Ausfall berichtet. (siehe Link). Es ging damals darum, dass die Oszilloskope plötzlich ihren Dienst verweigerten – manchmal auch mit einem Knall und anschließendem Geruch nach „Strom“. Oder es passierte einfach gar nichts nach dem Einschalten. Der Grund dafür war und ist immer wieder der Ausfall des verbauten 12VDC Netzteils CCH0123F1-Z03A. Das Oszilloskop ist so konstruiert, dass das Netzteil bei ausgeschaltetem „Hauptschalter“ des Oszilloskops trotzdem am Netz hängt und im Standby-Modus betrieben wird. Der an der Frontseite des Gerätes befindliche Druckschalter schaltet das Netzteil dann in den PowerON Modus und die 12V Leistung ein. Wenn die Geräte in den Labors permanent an bestromten Steckdosen hängen, verwundert es auch nicht, dass die Geräte schneller altern, als die guten alten Kisten mit den Kathodenstrahlröhren. Die Teile, die der permanenten Stromversorgung durch thermische Dauerlast zu Opfer fallen, habe ich, sowie auch den Reparaturaufwand im damaligen Beitrag dargestellt. Von Seiten der Vertriebsfirmen ist auch ein Nachbestellen oder eine Ersatzlieferung von neuen Netzteilen nicht vorgesehen oder gewünscht. Wenn die Geräte innerhalb der Garantie- Gewährleistungszeit ausfallen sollten, ist der Austausch durch den Hersteller kein Problem. Fallen die Geräte aber erst aus, wenn sie schon ein paar Jahre im Labor oder der Werkstatt verweilen (dabei spielt es aber keine Rolle ob sie jeden Tag in Betrieb sind, oder eben nur angeschlossen und ausgeschaltet herumstehen), so läuft ein üblicher Reparatur- Serviceprozess beim Hersteller. Da sind dann auch die ordentlichen Tarife für den Service von Messtechnik zu bezahlen.

Im Bild oben: „neues Meanwell Powersupply“ unten: „origales Lineage Power“

Die Netzteile sind, wie im alten Bericht beschrieben, recht gut zu reparieren. Allerdings ist die Reparatur auch ziemlich zeitaufwendig. Schneller geht es natürlich, wenn ein neues Netzteil eingebaut wird. Die Vertriebsfirmen der Keysight Oszilloskope bieten leider keinen Ersatzteilsupport an und einen direkten Lieferanten des original verbauten Lineage Power Netzteils habe nicht finden können. Es gibt aber auch eine andere Alternative: Im Forum des EEV-Blogs haben einige User alternative Netzteile gefunden, die in die DSO-X Oszilloskope passen. Ein passendes Modell ist das RPSG-160-12 von Meanwell. Es handelt sich dabei um ein 12V 160W Powersupply. Die Bezeichnung „G“ in RPSG deutet darauf hin, dass auch ein 5V Standby-Supply vorhanden ist. Und genau diese Funktion benötigt auch das DSO-X. Denn wie schon zuvor beschrieben, ist der Frontschalter am Oszi nicht dazu gedacht, die Primärseite der Netzversorgung zu trennen, sondern lediglich eine Leitung am DC-Niedervoltstecker gegen Masse zu schalten. Diese Leitung steuert im Netzteil den „PowerON-Pin“.

Mechanisch passt das Meanwell beinahe auf die Montagehalterungen des DSO-X. „Beinahe“ bedeutet, dass der Abstand der Befestigungslöcher der Längsseite am Powersupply um etwa einen Millimeter weiter auseinander liegt, als die Befestigungsbolzen am Chassis des Oszilloskops. Das lässt sich aber mit einer kleinen Rundfeile oder einem 4mm Bohrer schnell anpassen. Jetzt kann das Meanwell Powersupply mit den originalen Torx Schrauben am Oszi Chassis befestigt werden. Die Steckverbindung für die AC-Versorgung von der OSZI-Platine zum Netzteil kann direkt vom alten Netzteil übernommen werden.

Pinout der Meanwell Steckerleisten

Die 12V Spannungsversogung am CN2 des Netzteils liegt an den Pins 1,2,3,4 (+12V) und 5,6,7,8 (GND) an. Die Verbindungsleitung zum Oszi ins entsprechend anzupassen.

DC12V Ausgänge an CN2

Im Bild unten sind die Pins der Steckerleiste am Oszi beschriftet dargestellt.

DC12V Eingang ins Oszilloskop

Ich habe die Drähte passend umgepinnt und den Stecker wie unten dargestellt mit dem Powersupply verbunden.

Die Hauptenergieversorgung zum Oszilloskop ist jetzt hergestellt. Es fehlt nur noch die „Einschaltleitung“ (PowerOn). Hierzu habe ich den 7. Pin (GND) und den 9.Pin(Switch) aus dem alten Stecker gelöst und direkt auf dem Standby Board des Netzteils angelötet. Der Draht am untersten Pin des Standby Boards ist das Signal „PowerOn“ und der darüber ist GND. Somit kann das Netzteil mit dem frontseitigen Einschalter am Oszilloskop hochgefahren werden.

„Steuerleitungen“ für das PowerOn des Netzteils

 

Gesamtansicht der Verkabelung

Nach einem kurzen Funktionstest und Überprüfen der Spannung (kann ggf. auch an dem Trimmpoti am Netzteil korrigiert werden) ist der Umbau abgeschlossen und der Zusammenbau kann wieder erfolgen.

EVU Smartmeter mit ESP32 auslesen und Daten per MQTT senden

Loading

So nach und nach bringe ich viele meiner Smarthome Komponenten auf einen gemeinsamen Standard. Dabei habe ich mich entschieden, sämtliche Geräte über einen NodeRed Server zusammen zu führen. Auch das HomeMatic System kommuniziert mit NodeRed. Hier übergebe ich unter anderen auch die Messwerte des EVU-Zählers (bei mir ist ein Siemens IM350 Smartmeter verbaut) an die HomeMatic CCU. Dies geschieht wie schon einem früheren Beitrag erwähnt, über die LED-Impulsschnittstelle (1000 Impulse/kWh). Hierzu wird einfach ein Fototransistor über der LED am Zähler angebracht, der die Blinkimpulse der LED erkennt und in der Zählersensor-Sendeeinheit HM-ES-TX-WM in die Momentanleistung umrechnet und über die Zeit integriert und die Daten dann an die CCU weitersendet. Das funktioniert an sich ganz gut. Nur die Aktualisierungsrate (im Minutenbereich) ist mir zu lange. Auch scheint der Fototransistor immer wieder auf das Streulicht der benachbarten LED (diese zeigt die Blindleistung in 1000 Impulse/kvarh an) zu reagieren. So entstehen Abweichungen zwischen der Zählung über den HomeMatic Sensor und den direkt am Zähler abgelesenen Werten.

Das geht auf jeden Fall genauer. Wenn man sich den IM350 Smartmeter Zähler im Detail ansieht, bzw. das Manual durchliest, so stellt man schnell fest, dass er eine sog. „Kundenschnittstelle“ besitzt. Diese Kundenschnittstelle stellt einige Messdaten über eine galvanisch getrennte Datenleitung im Sekundentakt zur Verfügung. Dazu gehören unter anderen die momentane Wirkleistung in beiden Richtungen, sowie die Zählerstände von Wirk- und Blindleistung in Bezugs- und Einspeiserichtung. Also perfekte Ausgangbedingungen, um den HomeMatic Zählersensor durch eine eigene Konstruktion zu ersetzen. Nach ein wenig Internetrecherche habe ich schnell erkannt, dass ich nicht der Einzige bin, der sich mit genau dieser Thematik beschäftigt. Die Daten der Kundenschnittstelle purzeln nach Anforderung über eine Daten-Request Leitung mit einer Geschwindigkeit von 115kbaud heraus. Sie sind allerdings verschlüsselt, und nicht direkt lesbar. Um den 16 Byte langen Entschlüsselung Key zu erhalten, muss der Energieversorger konsultiert werden. Der Schlüssel ist an die Seriennummer des Smartmeters gebunden und für jedes Smartmeter individuell. Nach einigem Telefonieren mit meinen Kärntner Energieversorger wurde mir der Key-Code per Mail zugesandt. Im nächsten Schritt testete ich mit einem USB-UART Adapter an einem PC, ob bei korrekter Beschaltung der Schnittstelle auch wirklich Daten aus dem Zähler herauskommen. Dazu habe ich einen RJ11 Stecker auf ein geeignetes 6pol. Kabel gekrimpt und das offene Ende des Kabels entsprechend des Datenblattes des Zählers beschaltet. Dazu ist nicht besonders viel notwendig. Eine 5V Versorgung muss die Schnittstelle aktivieren, ebenso muss auch die Data Request Leitung an 5V geschaltet werden und schon stehen an der Data Out Leitung die Datenpakete an. Es funktioniert übrigens auch mit einer 3V3 Versorgung. Mit einem Terminalprogramm am PC (ich verwende meist putty oder hterm) kann man die verschlüsselten Daten visualisieren.

Jetzt ging es daran, sich zu überlegen, wie die Daten entschlüsselt und aufbereitet werden. Hierzu findet man mit Netz zwei Ansätze:

* über einen RaspberryPi, mit einer Python-Umgebung und einem Python Skript. Die Skripten übernehmen hier den Empfang und die Entschlüsselung der Daten und stellen sie dann auf unterschiedliche Weise zur weiteren Verarbeitung zur Verfügung

* über einen ESP32. Der ESP ist ebenfalls in der Lage eine 128Bit AES Verschlüsselung zu dekodieren und hat noch reichlich Ressourcen um die Daten aufzubereiten und über WiFi zu versenden. Außerdem ist ein ESP für wenig Geld in ausreichender Stückzahl verfügbar. Also habe ich mich für diese Lösung entschieden. Dazu gibt es auf GitHub ein open source Projekt vom User https://github.com/Andre-Schuiki/esphome_im350  in dem er einen ESP32 IM350 Decoder als Basis für eigene Projekte zur Verfügung stellt. Mit seinen Sourcen erhält man einen Decoder der die Zählerdaten im Sekundentakt ausliest und über die USB UART Programmierschnittstelle und auch via Telnet über WiFi ausgibt. Dieses Sourcen habe ich als Basis verwendet. 

Mein Ziel ist es, die aus dem Smartmeter gewonnenen Daten in MQTT Messages zu verpacken und an meinen MQTT Broker zu senden. Ab da ist es dann ein Einfaches, sie in NodeRed und die HomeMatic CCU zu bekommen und dort zu speichern. Also habe ich den Code angepasst.  Dazu wurde die Wifi Verbindung zum Router auf eine statische IP gesetzt. (sind in settings.h zu definieren). Die ausgelesenen Messwerte, sowie die RSSI der WiFi Verbindung, werden jetzt über MQTT Topics zur Verfügung gestellt. (die IP-Adresse zum Broker ist auch in settings.h zu definieren). Wenn man den Code jetzt kompiliert und auf den ESP jagt, dann sollte er sich in das jeweilige Netzwerk einbuchen. Solange der ESP noch auf einem PC-hängt, kann man über die Programmierschnittstelle und ein Terminal auch gleich überprüfen was er tut. Verbindet man jetzt den RJ11 Stecker mit der Kundenschnittstelle des Zählers, dann solle im Display des Zählers im Sekundentakt das Dreieck über der Beschriftung „KU“ blinken. Passiert das, dann sollten auch schon im Terminal die Messwerte stehen (vorausgesetzt man hat den KEY vom EVU nicht vergessen in secrets.h einzugeben). Klappt auch das, dann stellt ein Blick auf den MQTT-Broker (mit z. Bsp.: MQTT-Explorer) sicher, dass die Messages auch ankommen. Jetzt kann der ESP vom PC entfernt werden.

Anschlussbelegung
ESP32 im „freifliegenden“ Testaufbau

Ich habe eine sehr einfache Lösung gewählt und des ESP auf einer Lochrasterplatine befestigt. Das 6polige Kabel zum Smartmeter dort angelötet. Auf der Lochrasterplatte finden dann noch die Pull-Up Widerstände und ein NPN Transistor (BC547 etc.) zum Invertieren der Datenpulse Platz. Die Platine habe ich in einem kleinen Kunststoffkästchen untergebracht, dass jetzt lediglich mit einem Kabel an der Kundenschnittstelle und mit einem USB Kabel an einem USB Steckernetzteil angeschlossen ist.

Der fertige Aufbau sieht dann (bzw. zurzeit) so aus. Die Daten landen im MQTT Broker und NodeRed visualisiert sie und schickt sie auch zur HomeMatic CCU.

so kommen die Daten im MQTT Broker an
und können zum Beispiel so in NodeRed verarbeitet werden

wenn jemand an den angepassten Skripten interessiert ist, kann ich die gerne zusenden. Betreffend einer Veröffentlichung auf  GitHub muss ich mich erst informieren welche Lizenzbedingungen betreffend des ursprünglichen Repositorys zu erfüllen sind. Es wird dann hier (public) verfügbar sein:

https://github.com/ingmarsretro/esphome_im350/tree/main/standalone_version_mqtt

Die Wärmepumpe (NEURA) in das Smarthome einbinden

Loading

Ein Smarthome ist heute keine Seltenheit mehr und sehr weit verbreitet. Es gibt unzählige Systeme am Markt, die das eigene Zuhause „Smart“ machen. Die digitalen Sprachassistenten von Google, Amazon und co. in Verbindung mit Smarten Glühlampen zählen zu den einfach und schnell zu installierenden Systemen. Aber es gibt auch komplexe Smart Home Systeme, bei denen in den Hausverteilern Aktoren für jede Lampe und Steckdosen verbaut sind. Die Fenster und Türen sind mit Meldekontakten ausgestattet und sichern das Eigenheim oder melden, wenn einmal auf das Schließen der Fenster nach dem Stoßlüften vergessen wird. Das diese Systeme bei vernünftiger Programmierung auch zur Energieoptimierung beitragen ist selbstverständlich. Auch ich betreibe Smarthome Komponenten unterschiedlichster Hersteller.

Dazu gehört seit Jahren das HomeMatic System, das sowohl kabelgebunden als auch über das Bidcos-Protokoll mit seinen Aktoren und Sensoren kommuniziert. Das HUE – System von Phillips spricht dabei über ZigBee mit seinen smarten Lampen und Steckdosen. Die Gateways dieser Systeme sind an ein LAN Netzwerk angeschlossen und jedes System bringt seinen eigenen Webserver mit, über den es dann zu steuern und einzustellen ist. Ein Wechselrichter von Photovoltaikanlagen kann seine Daten über unterschiedlichste Schnittstellen (RS485, CAN, RS232) zur Verfügung stellen. Um alle auf eine zentrale Darstellungsebene zu bringen, habe ich mich für das NodeRed System entschieden. Der Dazu notwendige NodeRed Server läuft auf einem Raspberry PI. (Auf der CCU3 mit dem Raspbian Image ist noch genug Platz um den NodeRed Server laufen zu lassen – der ist sogar als eigenes Plugin für die CCU verfügbar und wird „RedMatic“ genannt).  Mit dieser Konfiguration lässt sich fast alles im Bereich Homeautomation „erschlagen“. Mit ESP32 und Raspberry lassen sich über MQTT (Message Queueing Telemetry Transport) bequem Statusinformationen übertragen. Dies wende ich beispielsweise bei den kleinen Einspeise Wechselrichtern einer Balkon PV-Anlage an, als auch bei den PV-Wechselrichtern einer Offgrid-Anlage. Hier werden die Daten über unterschiedliche Bussysteme im Raspberry oder ESP32 empfangen und in das MQTT-Protokoll umgesetzt. Der MQTT Broker sammelt die Daten der einzelnen Geräte und über NodeRed lassen sie sich dann in eine Datenbank schreiben, im Browser oder am Smartphone visualisieren und auch einfach, je nach Bedarf, im HomeMatic System verarbeiten.

Beispiel eines Smarthomenetzwerks

Somit ist es möglich, nahezu alle Systeme miteinander Smart zu vernetzen und, für mich wichtig auf EINER Plattform zu visualisieren. Ein einziges System fehlte bisher noch. Das ist meine alte Neura Heizungswärmpepumpe. Die Firma Neura ist schon seit einigen Jahren nicht mehr existent und der von „b.i.t.“ entwickelte auf Webserver „webidalog“ wurde nie mehr aktualisiert. Die Wärmepumpe hat also einen Webserver auf einem kleinen mit Linux-Rechner onboard und baut die Webapplikation mit einer uralten Java Version. Für die Bedienung muss am PC eine Java Runtime installiert sein, die nur mit einigen Tricks auf einem aktuellen Windows Rechner läuft (Stichwort: Virtualisierung). Für die Bedienung über ein Smartphone ist eine html – Version mit eingeschränkter Funktionalität verfügbar. Mein Plan war es nun, eine Schnittstelle zu finden, mit der ich die Daten der Wärmepumpe zumindest einmal auslesen kann, um Vorlauf- Rücklauftemperaturen der Fußbodenheizung, Kesseltemperatur, etc. auch in meinem NodeRed System zur Verfügung habe. Da zu dem System aber so gut wie keine Dokumentation zu finden ist und ein Reverse-Engineering ein wenig kritisch ist, wenn das System weiter laufen soll, kam mir folgende Idee:

Mit einem „headles browser“ sollte es ja möglich sein, die html-Version der Neura WebDialog Website zu parsen und die relevanten Daten zu finden und über Variablen in MQTT-Topics zu verwandeln. Und hier muss ich einen besonderen Dank an meinen Kollegen Mario Wehr aussprechen, der mir die Softwarestrukur zum parsen der Website gebaut hat. Die Software ist in PHP geschrieben und läuft schlussendlich auf einem Raspberry PI. Hier sind lediglich eine php8-cli runtime und ein paar Module notwendig. Die Software funktioniert so, dass bei jedem Aufruf ein Login auf der Wärmepumpenwebsite ausgeführt wird, danach werden die Daten geparsed und zu MQTT-Broker gesendet. Das kontinuierliche Aufrufen des php-Skriptes habe ich dann einfach mit einem cronjob gelöst, der jede Minute ausgeführt wird.

 

>sudo crontab -e

und der job sieht dann so aus:

* * * * * sudo php /home/neura2mqtt/neura2mqtt.php -c

(wenn man sich die files  ins  /home/ verzeichnis legt…). Das Projekt habe ich auf github unter:  https://github.com/ingmarsretro/neura2mqtt veröffentlicht.

Neura Daten am NodeRed Dashboard

 

 

 

 

Ein Nachbauprojekt für die Vectrex

Loading

Es ist wieder einige Zeit vergangen, dass ich es schaffe, in den späteren Abendstunden Zeit und Energie zu finden, hier im Blog über eines meiner kleinen Projektchen zu schreiben. Ich habe mir in den letzten Jahren angewöhnt, bei Autofahrten und nächtens, Podcasts zu hören. Dazu gehören in erster Linie Podcasts zu technischen Themen. Darunter ist auch ein Podcast, der sich „Retrokompott“ nennt und sich mit Homecomputern und Technik aus unserer Jugendzeit beschäftigt. Deren Slogan lautet:

Retrokompott, eine Zeitreise in die Vergangenheit alter Homecomputer, Spielekonsolen und Games

[http://blog.retrokompott.de/]

In einem der Beiträge von Retrokompott diskutierte man einige Folgen lang (172-177) über die Vectrex, den Heim – Vectorspieleautomaten von MBE. Hier wurden unter anderen auch Homebrewprojekte, also Software-Eigenentwicklungen der Anwender vorgestellt.  „Vectorblade“ ist dabei ein Spieletitel, der von Malban [http://vide.malban.de/] entwickelt wurde. Das Projekt wurde dabei mit dem ebenfalls von Malban entwickelten Vectrexcompiler (vide) erstellt. Die Sourcen sind öffentlich auf der Website verfügbar. In dem „Kompott“-Beitrag hat man so begeistert über Vectorblade berichtet, dass mein Interesse dafür geweckt war. Das Spielemodul war auch eine Zeit lang über Malban zu erwerben. Ich habe aber keine Quelle gefunden, über die ich das Modul auf einfache Weise erwerben kann. So dachte ich mir, baue ich mir das halt einfach nach. Das Besondere an diesem Gamerom ist die Größe des Games. Es hat stolze 192 kB. Um diesen Speicher adressieren zu können, hat sich Malban der Bank-Switching Technologie bedient. Er verwendet in seinem Design einen Flash-Speicher von SST, den SST39SF020. Das Bankswitching wird über einen Vierfach-2-Eingang NAND Schmitt Trigger (74AC132) gesteuert. Malban hat auf git das Layout veröffentlicht. Dort verwendet er den Speicher im DIL-Package und ebenso auch den AC132. Eine detaillierte Anleitung findet man hier.

Da ich von meinem alten Selbstbau-Rom Modul Projekt noch einige Platinen über habe, konnte ich schnell einen Versuchsaufbau zusammenstoppeln.  Flashspeicher hatte ich zwar keinen zur Verfügung – sehr wohl aber eine ausreichend großes EPROM. Der Vide-Compiler und die Source-Files sind auf Malbans GIT ebenfalls veröffentlicht. Nach kurzem Studium seines Vide-Compilers ist es mir gelungen das Projekt zu kompilieren und eine ROM – Datei zu erstellen. Mit meinem „Fernostprogrammer“ konnte ich dann das EPROM „brennen“.  Mit ein paar Drahtbrücken und einem AC132 wurde aus meinem alten ROM-Platinen Projekt dann der Vectorblade Versuchsaufbau.

Versuchsaufbau Vectorblade

Mit der Ausnahme, dass keine Settings gespeichert werden können, funktioniert der Testaufbau und das Game lässt sich spielen :). Der nächste Schritt des Nachbaus war dann die Platine zu zeichnen. Hier wollte ich den Schmitt-Trigger Baustein in SMD Ausführung einbauen und den SST weiterhin in DIL. Ich habe diese Ausführung auch realisiert und erfolgreich getestet. Es gibt aber einen kleinen Haken – keiner meiner Lieferanten hat den SST39SF020 Flashspeicher in DIL Ausführung auf Lager. Ich habe jetzt zwar einige Platinen mit DIL – Layout aber eben keine Chips… Also noch einmal zum PC und das Design auf PLCC Sockel umzeichnen. Gedacht – getan und einen Satz Platinen beim Fernostproduzenten bestellt.

Ein passendes Gehäuse lässt sich mit dem 3D-Drucker selbst erstellen. Genauer gesagt wurde ich auf Thingiverse fündig und konnte  aus einer Vielzahl an geeigneten Designs wählen.

Es fehlt zwar das Overlay –  aber auch ohne das macht das Spiel Spass. Hier ist Malban ein tolles Game gelungen.

MIDI DB50XG – ein Interface für das Daughter Board

Loading

Beim Stöbern in einer Kiste mit meinen alten Bastelarbeiten ist das folgende Kästchen zum Vorschein gekommen.  Es stammt aus der Zeit als ich noch mit Amgia, aber auch schon mit PCs zu tun hatte – ich schätze so ca.  um 1996. Das Kästchen beschriftete ich mit „DB50XG MIDI – Wavetableprozessor“.

Das Fundstück aus der Kiste

Darin befindet sich eine Platine von Yamaha, die sich eben DB50XG nennt. Diese Platine war als Tochterplatine für PC-Soundkarten mit „Waveblaster“ Erweiterungsport konzipiert. Sie erweiterte die Soundkarten um einem polyphonen MIDI – Wavetable – Sampler. So konnte der General Midi Standard und der Yamaha XG Standard wiedergegen werden.  Heute macht sich darüber niemand mehr Gedanken. Wenn man damals mit einem PC aus Midi – Daten Sounds erzeugen wollte, dann war entweder eine externe Hardware notwendig, oder eben eine Soundkarte mit einem OnBoard Midi Synthesizer oder Wavetable Chipsatz. Der PC übernahm dann die Steuerung, das Senden und Empfangen der Midi Daten über eine Sequenzer Software. Heute werden die Midi Sounds direkt am PC generiert und die Samples und Tonmodelle in die Software eingebunden. Damals reichte die Leistung der PC-Hardware dazu nicht aus. Wenn sich jetzt jemand gerade fragt, worüber ich hier palavere – was ist Midi und wofür benötigt man das? – dann sei hier kurz gesagt: Midi ist die Abkürzung für „Musical Instrument Digital Interface“ – also eine digitale Schnittstelle – ein Datenprotokoll für Musikinstrumente. Es dient – grob erklärt – dazu, elektronische Musikinstrumente untereinander zu vernetzen und zu steuern. So kann zum Beispiel über ein einziges Keyboard eine Vielzahl von klangerzeugenden Geräten gesteuert werden. Wie der Midi Standard funktioniert, wie die Datenpakete aussehen und das elektrisch aussieht, werde ich hier nicht erläutern. Dazu findet man, wie immer, reichlich Informationen im Netz.

Im Inneren des Kästchens

Zurück zum selber gebastelten Kästchen. In die Plastikbox habe ich damals das DB50XG gepackt und vom „Waveblaster“-Port, einer 26poligen Buchsenleiste, die notwendigen Leitungen zur Inbetriebnahme der Midi Platine nach Außen geführt. Und das war ziemlich simpel. Die Platine benötigt eine Spannungsversorgung von +/-12V und +5V. Es gibt einen Midi-IN und einen Midi-OUT (Through) Pin, einen Reset-Pin und zwei Analog Audio Out Pins – je einen pro Kanal. Die untenstehende Tabelle zeigt die Pinzuordnung des Steckers:

Pin Nummer Zuordnung
1 Digital Masse
2 nicht verbunden
3 Digital Masse
4 nicht verbunden
5 Digital Masse
6 Versorgung +5V
7 Digital Masse
8 nicht verbunden
9 Digital Masse
10 Versorgung +5V
11 Digital Masse
12 nicht verbunden
13 nicht verbunden
14 Versorgung +5V
15 Analog Masse
16 nicht verbunden
17 Analog Masse
18 Versorgung + 12V
19 Analog Masse
20 Audio out rechts
21 Analog Masse
22 Versorgung -12V
23 Analog Masse
24 Audio out links
25 Analog Masse
26 Reset

Der ganze Aufbau war damals eher sehr spartanisch gestaltet. Die Stromversorgung musste über ein, oder mehrere externe Netzteile hergestellt werden. Es gab keine galvanische Signaltrennung mittels Optokoppler. Da musste ich mich auf den ordentlichen Aufbau des Midi-IO-Controller verlassen, den ich an den Amiga angeschlossen hatte. So durfte das natürlich nicht bleiben. Und das schöne DB50XG Board nicht mehr zu verwenden, oder dem Elektronikschrott zuzuführen, bringe ich nicht über´s Herz. Der Plan der daraus entstand, war, ein neues Interfaceboard zu entwickeln – oder basteln, das möglichst universell einsetzbar werden sollte. 

DB50XG

Diese Idee ist nun schon wieder einige Jahre her und immer wieder einmal habe ich ein wenig daran gearbeitet. Folgende Punkte, so habe ich mir ausgedacht, sollte das Interfaceboard erfüllen:

  • eine einfache Spannungsversorgung soll das Yamaha Board mit Energie versorgen. Idealer Weise soll ein USB-Port und optional ein Anschluss für ein Universalnetzteil vorhanden sein. Alle benötigten Spannungen sollen auf dem Interfaceboard aus den 5VDC generiert werden.
  • Das DB50XG soll, wie seinerzeit, auch als „Huckepack“ Platine aufgesteckt werden können
  • Das Midi-in Signal soll über die 5polige DIN Buchse und auch über einen Pinheader eingespeist werden können – natürlich schön entkoppelt (Damit kann auch ein Microcontroller wie Arduino und co. ganz ohne Aufwand angeschlossen werden)
  • Der Ton, also das Audiosignal soll pro Kanal über je eine Chinch-Buchse und auch als 3.5mm Klinkenbuchse und über einen Pinheader zur Abnahme bereitstehen.
  • Wortwiederholungen SOLLTEN vermieden werden, ist mir aber egal 🙂

Daraus entstand schlussendlich der folgende Schaltplan. Die 5VDC Versorgung der USB Quelle wird direkt zur 5V Versorgung des Midi Boards geführt. Die ebenfalls benötigten +12V/-12V erzeugt ein DC/DC Converter (TMR0522). Dieser wird eingangsseitig vom 5V Netz versorgt. Der optionale „Externe“ Spannungseingang gelangt an einen LM2596ADJ. Das ist ein Step-Down Voltage-Regulator der mit Eingangsspannungen bis zu 40V arbeiten kann. Die geregelte Ausgangsseite ist in vielen Bereichen verfügbar. Ich habe hier den ADJ (Adjustable) Typ in die Schaltung integriert, da ich davon einige Stück im Sortiment Kasten habe. Durch einen Jumper am Board ist, die Spannungsquelle wählbar.

Auf Basis dieses Schaltplanes habe ich ein Layout erstellt und es vorerst einmal im eigenen Ätzbad hergestellt. Heraus kam die folgende Platine, die als Testaufbau diente. Technisch funktionierte das Board einwandfrei, jedoch die Anordnung der Komponenten hat mir nicht gefallen. Den Step-Down Converter samt Spule hatte ich auf der Rückseite platziert. Auch war mir der Abstand zwischen den Anschlussbuchsen zu eng beieinander. Und wie man das als PCB Layouter so macht – man macht immer ein zweites Design. So auch dieses Mal.

Der Testaufbau mit bestücktem Midiboard ist im nachfolgenden Bild zu sehen. Das Midi-Signal als Testquelle kommt vom PC und wird durch einen USB-Midi Adapter aus Fernost generiert.

Also noch einmal vor den Rechner gesetzt und das Layout umgezeichnet. Heraus gekommen ist dann die folgende Version. Diese Ausführung habe ich dann bei einem Leiterplattenhersteller bestellt.

Die schlussendlich gefertigte Platine in bestücktem Zustand sieht dann so aus. Darunter ist sie  mit dem aufgesteckten DB50XG Board zu sehen.

 

Wenn der Spiegel nicht mehr klappt

Loading

Immer öfter höre und lese ich von nicht mehr richtig funktionierenden elektrisch einklappbaren Außenspiegeln bei Fahrzeugen des deutschen Herstellers mit den vier Ringen. Das Problem tritt bei vielen Modellen auf, die schon ein paar Jährchen in Betrieb sind und in unserem hiesigen Klima betrieben werden. In Internetforen findet man einige User, die dieses Problem kennen. Auch in meinen Bekanntenkreis gibt es ein paar Ringe-Fahrer die einen klemmenden elektrischen Außenspiegel haben. Als Lösung wird vom Hersteller natürlich immer der Austausch der kompletten Einheit empfohlen. Wer sein Erspartes aber nicht sinnlos für neu produzierten Restmüll ausgeben möchte, kann sich selbst dieses Problems annehmen. Es ist sogar eine ziemlich kleine Ursache, die dieses Problem verursacht. Und das Beste – es lässt sich ohne Materialaufwand reparieren. Auch ist die Langlebigkeit der Reparatur mittlerweile bewiesen…

Der Fehler zeigt sich durch folgendes Verhalten:

  • der Spiegel macht quietschende, knarrende Geräusche beim Aus – Einklappen
  • der Spiegel bleibt an falscher Position stehen und lässt sich nur durch manuelles Bewegen einrasten
  • das Klappverhalten ist abhängig vom Wetter

 

Man liest darüber viele Beiträge mit möglichen Ursachen – von defekten Motoren und defekten Türsteuergeräten. Am besten sollte man gleich die Spiegeleinheit erneuern und dazu ein neues Türsteuergerät – ja klar …

Die Lösung des Problems ist einfacher: ein kleiner Stahlbolzen, der von einer kleinen Feder rausgerückt werden soll, bleibt in seiner Führung stecken. Der mechanische Bereich des Spiegels ist natürlich auch den Umweltbedingungen ausgesetzt und so kommt der Bereich mit Regen, Spritzwasser – im Winter Salzwasser in Kontakt. Im Laufe der Zeit verlieren die Schmierstoffe ihre Eigenschaften oder werden sogar ausgewaschen und das ganze „Werkl“ wird schwergängig. Also was hilft? Komplett zerlegen, reinigen neu schmieren und wieder zusammenbauen.

Ich habe für diesen knapp eineinhalbstündigen Eingriff damit begonnen, den Spiegel aus der Tür auszubauen und in der gemütlichen Werkstatt zu untersuchen. Dazu ist die am einfachsten die Innenverkleidung der Türe abzunehmen (je nach Fahrzeug ein paar Schrauben und viele Klipse…) Der Spiegel ist dann mit einem Kabel am Türsteuergerät angesteckt und mit Torx-Schrauben befestigt.

Das Spiegelglas lässt sich am einfachsten mit einem Plattenheber (Saugnapf) ausklicken. Dann sind vorsichtig – falls vorhanden- die beiden Flachstecker von der Spiegelheizung abzuziehen (unbedingt die Kontakte auf der Heizfolie gegenhalten). Als nächstes können beiden Kunststoffhälften des Spiegel Gehäuses entfernt werden. Hier hilft ein wenig Beobachtungsgabe, welche Schrauben zu entfernen sind und wie die Hälften zusammengehalten werden.

Jetzt liegt das Kernstück des Spiegels da. Die beiden Druckgussteile sind über eine hohle Achse miteinander verbunden. Durch die Achse führt das Anschlusskabel zum Spiegelverstell-Antrieb und zur Heizung. Über der Achse sitzt eine große Stahlfeder die mit einer Distanzscheibe und einem Spannring (keine Ahnung, ob das die korrekte Bezeichnung ist) befestigt. Die Feder übt einen ordentlichen Druck zwischen den beiden Teilen aus- und das ist jetzt der einzige etwas schwierigere Teile – die Feder muss raus. Dazu ist der Spannring auszuhebeln, während die Feder auf Spannung gehalten wird. Heraus geht sie einfach – aber das wieder einbauen wird zur Herausforderung, wenn man kein geeignetes Werkzeug hat.

Auf dem Bild ist die schon entspannte Feder zu sehen. Jetzt können die beiden Teile auseinandergenommen werden.

Hier sind die Teile in zerlegter Form zuerkennen. Um nun das Corpus Delicti zu erreichen, muß das kleine Getriebe mit dem Motor abgeschraubt werden. Darunter ist der Bolzen zu erkennen, der in diesem Fall fest in seiner Bohrung steckte, sodaß es der Feder nicht mehr gelungen ist, ihn heraus zu drücken.

Deckel des kleinen Getriebe
Bolzen ist links neben dem Befestigungsloch zu erkennen

Bolzen mit Feder
auch die Führung des Bolzen ist zu reinigen

Die Prozedur ist ziemlich einfach – alles reinigen, die Korrosionen entfernen und mit Schmierstoffen neu abschmieren. Danach wieder alles zusammenbauen sich freuen. 🙂 Die meiste Zeit der ganzen Arbeit bnimmt das Reinigen in Anspruch.

Übrigens: der hier beschriebene Spiegel stammt von einem A5…

UV-Sensor Logger selbstgebastelt

Loading

Kommt der Sommer, kommen neue Ideen. In den Sommermonaten ist ja bekanntlich die Sonnenscheindauer länger und auch die Intensität der Sonnenstrahlen höher. Viele nutzen diese Eigenschaft der Sonne, um ihre Vitamin-D Produktion des Körpers anzutreiben, andere wiederum legen sich unter die Strahlenquelle um durch den hohen UV Anteil ihrer Hautfarbe abzudunkeln. Dies wiederum steigert vermeintlich deren Attraktivität und regt die Hormonproduktion und die Paarungsbereitschaft an… Leider hat der nicht sichtbare UV Bereich im Spektrum des Sonnenlichts bekanntlich auch negative Auswirkungen auf den menschlichen Körper. Auch technisch kann das Sonnenlicht genutzt werden. Durchschnittlich wird die Leistung der Sonne pro Flächeneinheit mit 1000W pro m² angenommen. Großflächige P-N Übergänge in Halbleitermaterialien schaffen mittlerweile mit einem Wirkungsgrad von bis zu 22% daraus elektrische Energie zu erzeugen.

Man kann die Energie aber auch noch anders nutzen, bzw. den UV-Anteil. Vielen Retrosammlern ist sicherlich das Problem mit den vergilbten alten Kunststoffgehäusen bekannt. Um das in den Griff, bzw. wieder in den Ursprungszustand von vor 30, 40 Jahren zu bekommen, verwendet man H2O2 also Wasserstoffperoxid und UV Licht um so einen Bleichprozess in Gang zu bekommen. Und so kam ich zur Idee für folgendes Projekt.

Bei einem online-Elektronik-Laden fand ich im Abverkaufs Angebot ein UV-Sensor Board des Herstellers Waveshare. Darauf befindet sich ein LITEON OPTOELECTRONICS LTR390 Chip samt Levelshifter-Schaltung. Als Interface steht ein I²C Bus zur Verfügung. Ein Blick ins Datenblatt verriet mir, dass der Sensor zwei Wellenlängenbereiche erfasst und separat ausgibt.  Der ALS (Ambient Light Sensor von 500-600nm) und der UV (Ultra Violett Bereich von 300-350nm).  Damit kann man doch schnell ein einfaches Logging Board basteln – dachte ich mir.  So habe ich mir gedacht, das Board sollte folgendes können:

  • Spannungsversorgung von einer 18650er Zelle oder USB
  • USB soll den Akku auch laden können
  • einen Micro-SD Slot zum Aufzeichnen der Sensordaten
  • einen RS-232 Port, zum direkten Loggen am PC
  • ein cooles OLED Display
  • zwei Taster zum Bedienen des Loggers (Intervall, Start/Stop etc.)

Die Steuerung soll natürlich wieder einmal ein Chip von Atmega – der 328er übernehmen. Davon befinden sich einfach noch genügend Stück in meinen Sortiment Kästchen. Damit man sich schneller einen Überblick über den Aufbau verschaffen kann, habe ich das folgende Blockdiagramm gezeichnet:

Im nächsten Schritt habe ich aus dem Blockschaltbild einen Schaltplan erstellt, um aus dem dann wiederum ein Layout erstellen zu können.  Parallel zur Schaltplanerstellung habe ich einzelne Bereiche per „Luftverkabelung“ auch gleich probeweise zusammengeschaltet und getestet, ob das alles auch so funktioniert, wie ich mir das vorstelle. Und vor allem sollte auch alles im Flashspeicher des Microcontrollers Platz haben.

Im Bild oben ist der „luftige“ Aufbau bestehend aus fertigen Komponenten zu erkennen. Für die ersten Tests mit dem Sensor und dem OLED Display reichte ein Arduino vollkommen aus. Damit war es mir möglich, die gewünschten Funktionen zu testen. Somit stand der Erstellung des Schaltplanes nichts mehr im Weg. Eine 18650er Lithiumzelle soll als primäre Energiequelle dienen. Alternativ wird auch ein USB-Port vorhanden sein, der die Zelle laden kann bzw. den Sensor betreiben kann. Dafür, weil ich faul bin und auch ziemliche Bauteil Lieferengpässe ein großes Problem sind, verwende ich zum Laden des Akkus eine fertiges Wemos-D1-Mini Board. Das wird genauso wie das OLED Displayboard und das Sensorboard als fertige Komponente auf dem Design der Platine Platz finden.  Als Controller kommt wieder, wie schon erwähnt, ein Atmega328 im TQFP Gehäuse zum Einsatz. Dieser wird über die I²C Schnittstelle mit dem OLED Display (SBC-OLED01 mit SSD1306 Controller) und dem LTR390 UV-Sensorboard kommunizieren. OLED und Sensor sind 5V kompatibel. Die SD-Karte wird aber mit 3.3V betrieben. Dafür benötigt die Schaltung noch einen Spannungswandler von 5V auf 3.3V für die Versorgung und einen Levelshifter für den SPI-Datenbus, über den die SD-Karte mit dem Atmega die Daten austauscht. Da der Atmega dann auch mit seiner Firmware programmiert werden möchte, habe ich einen 2×4 Pinheader für den Anschluss eines Programmers vorgesehen. Sechs Pins davon (GND,5V, MOSI, MISO, SCK und RESET) benötigt der Programmer und die zwei verbleibenden Pins sind für die serielle Schnittstelle vorgesehen. Die beiden Interrupt-Eingänge des Atmega werden mit je einem Taster beschalten, der dann die Software bedienbar macht. Die Batteriespannung wird über einen Teiler an einem der ADC-Eingänge gemessen bzw. auch mitgeloggt. Das Ergebnis dieser Gedanken ist der folgende Schaltplan:

Ein Layout ist danach der nächste Schritt.  Bei einer Größe von 12 x 4,5 cm ist die Platine einigermaßen „handlich“. Die Leiterbahnführung findet auf beiden Seiten statt und die Module (Ladeschaltung, Display und UV-Sensor) sind über Pinheader steckbar ausgeführt.

Die beiden Bilder oben zeigen die Vorschau der „Top-“ bzw. „Bottom-“ Seite des Layouts. Aus den so erstellten Produktionsdaten konnte eine Platine erstellt werden.

Nach einiger Lötarbeit war die Hardware dann soweit fertig. Um diesem „Lötwerk“  letztendlich auch Leben einzuhauchen, bedurfte es einer Software, die auf dem Microcontroller ihre Arbeit verrichtet.

Beim Basteln der Software bediente ich mich der kostenlosen „Arduino IDE“ Entwicklungsumgebung.  Die Dokumentation des LTR390 beschreibt genau über welche Register welche Funktionen des Sensors zu bedienen sind. Es gibt aber auch schon für ganz Bequeme eine fertige Library – so wie für fast alle Sensoren und Aktoren, die an Microcontroller angeschlossen werden sollen. In der Arduino IDE findet man über den Boardmanager die „Adafruit LTR390 Library“ über die man einfach mit dem Sensor kommunizieren kann.  Die Ansteuerung des OLED Displays übernimmt in meinem Fall die SSD1306Ascii Library. Die Buskommunikation übernehmen die „Wire“ und “ SPI“ Library und die „SD“ spricht mit der SD – Karte.  Die Includes sehen dann so aus:

#include <LTR390.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include „SSD1306Ascii.h“
#include „SSD1306AsciiWire.h“

Den gesamten Code kann ich bei Bedarf gerne hier veröffentlichen. Er ist allerdings kein Hexenwerk, sondern simples und sicher nicht optimiertes Codezeilen Geschreibe 🙂 In der derzeitigen Code- (Firmware) Version 1.3d gibt es ein kleines Auswahlmenü, das es ermöglicht, das Logintervall der SD-Karten-Aufzeichnung einzustellen und natürlich auch die Aufzeichnung zu starten bzw. zu stoppen. Geloggt wird in ein Textfile. Die aufgezeichneten Daten sind UV-Index, Umgebungshelligkeit und die Akkuspannung.

Einen Auszug aus dem Datalog habe ich unten eingefügt:

 Ambient[lx], UV-indx, Batt[V], Loggingintervall[s]  
 691.60,0.01,3.77,20  
 691.60,0.03,3.76,20  
 1184.00,0.03,3.77,20  
 1184.00,0.03,3.75,20  
 1191.00,0.03,3.77,20  
 1191.00,0.03,3.75,20  
 1198.60,0.03,3.76,20  
 1198.60,0.03,3.73,20  
 1211.60,0.03,3.76,20  
 1211.60,0.04,3.75,20  
 1223.00,0.04,3.75,20  
 1223.00,0.04,3.76,20  
 1234.20,0.04,3.76,20  
 1234.20,0.04,3.74,20  
 1243.60,0.04,3.76,20  
 1243.60,0.04,3.76,20  
 1252.00,0.04,3.75,20  
 1252.00,0.04,3.73,20  
 1261.20,0.04,3.74,20  
 1261.20,0.04,3.72,20  
 1269.60,0.04,3.76,20  
 1269.60,0.04,3.76,20  
 1278.40,0.04,3.76,20  
 1278.40,0.04,3.75,20  
 1288.40,0.04,3.76,20  
 1288.40,0.04,3.75,20  
 1298.20,0.04,3.76,20  
 1298.20,0.04,3.74,20  
 1305.80,0.04,3.73,20  
 1305.80,0.04,3.73,20  
 1313.20,0.04,3.73,20  
 1313.20,0.04,3.75,20  
 1321.60,0.04,3.74,20  
 1321.60,0.04,3.75,20  
 1331.80,0.04,3.75,20  
 1331.80,0.04,3.75,20  
 1341.60,0.04,3.74,20  
 1341.60,0.04,3.76,20  
 1349.40,0.04,3.76,20  
 1349.40,0.04,3.76,20  
 1358.20,0.04,3.72,20  
 1358.20,0.04,3.76,20  
 1365.60,0.04,3.74,20  
 1365.60,0.04,3.73,20  
 1374.20,0.04,3.72,20  
 1374.20,0.04,3.75,20  
 1380.60,0.04,3.75,20  
 1380.60,0.04,3.76,20  
 1386.60,0.04,3.75,20  
 1386.60,0.04,3.76,20  
 1394.80,0.04,3.75,20  
 1394.80,0.04,3.75,20  
 1401.40,0.04,3.73,20  
 1401.40,0.04,3.74,20  
 1408.60,0.04,3.75,20  
 1408.60,0.04,3.74,20  
 1414.20,0.04,3.73,20  

 

Diese Daten lassen sich jetzt sehr einfach weiterverarbeiten und grafisch darstellen. Als Office-Nutzer kann man zum Beispiel auf Excel zurückgreifen und die Daten dort importieren und als Graphen darstellen. Es geht aber noch einfacher und auch sehr schnell mit Tools wie Matlab. Mit einem Script wie dem nachfolgenden kann man die Logdatei dann visualisieren.

 

 %% Setup the Import Options and import the data  
 opts = delimitedTextImportOptions("NumVariables", 4);  
 opts.DataLines = [3, inf];  
 opts.Delimiter = ",";  
 opts.VariableNames = ["Ambientlx", "UVindx", "BattV", "Loggingintervalls"];  
 opts.VariableTypes = ["double", "double", "double", "double"];  
 opts.ExtraColumnsRule = "ignore";  
 opts.EmptyLineRule = "read";  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV"], "TrimNonNumeric", true);  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV", "Loggingintervalls"], "DecimalSeparator", ",");  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV"], "ThousandsSeparator", ".");  
 datalog = readtable("F:\ingmarsretro\datalog.txt", opts);  
 clear opts  
 x=size(datalog); % groesse der tabelle  
 measurement=x(1); % anzahl messungen   
 uvi=datalog{1:measurement,2};  
 ambient=datalog{1:measurement,1};  
 messzeit = linspace(0,(measurement*datalog{1,4}),measurement); %zeitvektor von 0 bis zeitintervall aus datalog spalte4 * messungen  
 figure(1);  
 title('UV - Index');  
 subplot(2,1,1);  
 plot(messzeit,uvi);  
 title('UV - Index');  
 xlabel('Zeit [s]');ylabel('UV - Index');  
 subplot(2,1,2);  
 plot(messzeit,ambient);  
 title('Beleuchtungsstärke');  
 xlabel('Zeit [s]');ylabel('Beleuchtungsstärke [lux]');  

Wird das Script ausgeführt, dann erhält man einen Plot, der die Messdaten visualisiert.

Die technischen Informationen zum Sensor sind dem Datenblatt des Herstellers zu entnehmen. Hier ein paar kurze Eckdaten:

Der LTR390 besteht aus zwei Fotodioden, einer für das sichtbare Spektrum des Lichtes und einer, die im UV-Bereich empfindlich ist. Der Strom der Photodioden wird in internen ADCs digitalisiert. Eine Interne Logic steuert die ADCs und über eine I²C Schnittstelle wird die Verbindung zur Außenwelt hergestellt. Die Auflösung von ALS und auch UVS ist in 13,16,17,18,19 und 20 Bit konfigurierbar. Der Sensor Chip ist in einem 2x2mm 6pin Gehäuse untergebracht. Die Detektoröffnung hat eine Kantenlänge von 280×280 µm.

Quelle: Datenblatt LTR-390UV https://optoelectronics.liteon.com/en-global/Led/LED-Component/Detail/926
Quelle: Datenblatt LTR-390UV https://optoelectronics.liteon.com/en-global/Led/LED-Component/Detail/926