's Werelds kleinste Home Assistant controller op ESP8266 en Espruino

Door Thijsmans op zondag 24 maart 2019 22:00 - Reacties (9)
Categorie: -, Views: 2.911

Mijn knutselprojecten zien vaak op het verzenden van data naar Home Assistant. Zo ook het project waarvan nu een prototype ligt te proefdraaien (spoiler!). En terwijl dat project ligt te testen, heb ik wat voorradige componenten bij elkaar geraapt en het doel eens omgedraaid: niet data verzenden, maar ophalen uit Home Assistant. En dat resulteerde onbedoeld in misschien wel 's werelds kleinste Home Assistant controller, van zo'n 30mm x 45mm... Uiteraard op Espruino!

Samenwerking gezocht! Heb jij een 3d-printer? Lees dan ook vooral even door tot onderaan! (na de code)

Het leuke aan de Wemos D1 Mini (een ESP8266-bordje) is dat er allerhande "shields" voor verkrijgbaar zijn, die je eenvoudig op de D1 "klikt". Je kunt de shields zelfs op elkaar stapelen. Zo zijn er bijvoorbeeld shields met DHT22-sensor (temperatuur en luchtvochtigheid), PIR-sensor (beweging), battery-pack en drukknop. Lego voor nerds makers, dus. In mijn voorraad ligt al een tijdje een shield met oled-schermpje met de geweldige resolutie van 64 x 48 pixels te wachten op aandacht - heeft iedereen zijn loep paraat? Gegeven het schermpje en mijn Home Assistant-installatie, ligt het voor de hand om die twee delen met elkaar te verbinden in de vorm van een fysieke controller.

Het resultaat is een schermpje ter grootte van een duimnagel met drie drukknopjes om door de sensoren, groepen, en schakelaars te bladeren, en de status van de getoonde entiteit te kunnen wijzigen. Voor het ophalen van de statussen en schakelen van de entiteiten wordt de Home Assistant API gebruikt; de data is dus altijd actueel. De controller toont elke vijf seconden de volgende entiteit. Blader je met de knopjes voor- of achteruit, dan blijft 'ie staan. Het automatisch doordraaien wordt hervat door de schakelknop langer dan een halve seconde ingedrukt te houden.

Een video zegt meer dan in deze hele blog zou kunnen staan.



In mijn vorige blog (de Espruino flash guide) legde ik stap voor stap uit hoe je Espruino op een ESP8266 flasht. Met onderstaande code bouw je dit project vervolgens gemakkelijk na. De code gaat ervan uit dat de wifi-instellingen al zijn opgeslagen (zie de code onderaan de flash guide). Je hoeft alleen de gegevens jouw Home Assistant-installatie in te vullen, waaronder een long-life token.

De tactile switches maken aan één kant verbinding met GND, aan de andere kant met de hieronder staande GPIO-pinnen. Als we die pinnen de modus 'input_pullup' geven, hoeven we geen externe weerstanden te gebruiken. Win!

JavaScript:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// Gegegevens van de Hass-instantie; maak een long-life token
// onderaan de pagina @ http://[hass-host]:8123/profile 
const hass_host  = '[JOUW HASS HOST]';
const hass_port  = 8123;
const hass_token = '[JOUW HASS LONGLIFE TOKEN]';

// Pin-out
const pin_i2c_scl = NodeMCU.D1;
const pin_i2c_sda = NodeMCU.D2;
const pin_sw_prev = NodeMCU.D6;
const pin_sw_next = NodeMCU.D5;
const pin_sw_toggle = NodeMCU.D7;

// Zie beschikbare oled-opties @ https://www.espruino.com/SSD1306
// Met oled_scale_value kun je kiezen uit een zo groot mogelijke 
// weergave (vector-font, maar niet altijd goed leesbaar) of een
// vaste grootte (scherper dus altijd leesbaar)
const oled_options = { contrast: 122 };
const oled_scale_value = false;

// Lijst met Home Assistant-entities. Geef in elk geval een 
// entity_id op. De friendly_name uit Home Assitant kan worden 
// overschreven door een 'label' te definieren
const panels = [
        { entity_id: 'sensor.hourly_gas_consumption', label: 'Gasverbruik' },
        { entity_id: 'sensor.power_consumption', label: 'Energieverbruik' },
        { entity_id: 'sensor.buiten_temperatuur', label: 'Temp. buiten' },
        { entity_id: 'light.staande_lamp' },
        { entity_id: 'light.plafondlamp' },
    ];

// Setup van libraries en niet-config variabelen
I2C1.setup( { scl: pin_i2c_scl, sda: pin_i2c_sda } );
var oled = require("SSD1306").connect(I2C1, render_panel, oled_options );
require("FontDylex7x13").add(Graphics); 
var http = require("http");
var panel_index = 0;
var cycle_itv   = 0;

// render_panel() bouwt het beeld op
function render_panel () 
{
    var panel = panels[ panel_index ];

    // Hass API-request opbouwen en uitvoeren
    request = {
            host: hass_host,
            port: hass_port,
            path: '/api/states/' + panel.entity_id,
            method: 'GET',
            headers: { "Authorization": "Bearer " + hass_token }
        };
  
    http.get( request, function(res) { 

        // In stukken ontvangen data aan elkaar plakken
        var json = '';
        res.on('data', function(data) { 
            json += data; 
        });

        // Alle data binnen => beeld opbouwen
        res.on('end', function () { 
            
            entity = JSON.parse(json);
            
            entity_label = panel.label ? 
                panel.label : 
                entity.attributes.friendly_name ;

            // Label bovenin plaatsen
            oled.clear();
            oled.setFontAlign( 0, 0, 0 );
            oled.setFontBitmap();
            oled.drawString( entity_label, 64, 20);
            
            entity_type = panel.entity_id.substr(0, panel.entity_id.indexOf('.'));
            switch( entity_type )
            {
                // Bij on/off-entiteiten, vertalen naar Nederlands
                case 'group': case 'light': case 'switch':
                    entity_value = entity.state == 'on' ? 'Aan' : 'Uit';
                    break;

                // Bij andere entiteiten, waarde + units weergeven
                default:
                    entity_value = entity.state + 
                        ( entity.attributes.unit_of_measurement ? 
                            ' ' + entity.attributes.unit_of_measurement : 
                            '' 
                        );
                    break;
            }

            // Bij vector-font, maximale grootte bepalen
            if( oled_scale_value )
            {
                value_size = 1;
                while( oled.stringWidth(entity_value) < 60 && value_size < 34 )
                {
                    value_size++;
                    oled.setFontVector( value_size );
                }
            } else
            {
                oled.setFontDylex7x13();
            }
  
            oled.drawString( entity_value, 64, 40);

            // Pager onderaan opbouwen
            oled.setFontBitmap();

            pager_char = '-';
            pager_char_active = '=';
            pager = pager_char.repeat( panels.length-1 );
            pager = pager.substr(0, panel_index) + 
                        pager_char_active + 
                        pager.substr(panel_index, panels.length );
            oled.drawString(pager, 64, 62);

            // Beeld naar scherm sturen
            oled.flip();
        });
    });
}

// Functies om door carousel te bewegen
function cycle_start ()
{
    cycle_itv = setInterval( function () {
            cycle_panel_up();
            render_panel();
        }, 5000 );
}

function cycle_stop ()
{
    cycle_itv = clearInterval( cycle_itv );
}

function cycle_panel_up ()
{
    panel_index++;
    if( panel_index >= panels.length )
        panel_index=0;
}

function cycle_panel_down ()
{
    panel_index--;
    if( panel_index < 0 )
        panel_index=panels.length-1;
}

function cycle_error ()
{
    if( cycle_itv > 0 )
    {
        cycle_stop();
        cycle_start();
    } else
    {
        // Toon foutmelding 2,5 sec
        setTimeout( function () { render_panel(); }, 2500 );
    }

    oled.clear();
    oled.setFontDylex7x13();
    oled.drawString('Computer', 64, 20 );
    oled.drawString('says', 64, 40 );
    oled.drawString('no', 64, 60 );
    oled.flip();

}

// Bedieningsknoppen maken gebruik van interne weerstanden
pinMode( pin_sw_prev, 'input_pullup' );
pinMode( pin_sw_next, 'input_pullup' );
pinMode( pin_sw_toggle, 'input_pullup' );

// Knop om terug te scrollen
setWatch( function () { cycle_stop(); cycle_panel_down(); render_panel(); }, 
        pin_sw_prev, { debounce: 25, repeat: true, edge: 'rising' }
    );

// Knop om vooruit te scrollen
setWatch( function () { cycle_stop(); cycle_panel_up(); render_panel(); }, 
    pin_sw_next, { debounce: 25, repeat: true, edge: 'rising' }
);

// Knop om te schakelen
setWatch( function (e) {

        // We willen alleen actie bij het omhoog komen van de knop, maar 
        // hebben de tijd nodig dat de knop was ingedrukt. Daarom deze 
        // controle op e.state (true = knop losgelaten), ipv het instellen 
        // van "edge: 'rising'" @ watch-options (zie hieronder)
        if( e.state )
        {
            var press_duration = e.time - e.lastTime;

            if( press_duration > 0.5 ) {
                // Knop meer dan een halve seconde ingedrukt, dan carousel hervatten
                cycle_start();
            } else  {
                // Knop kort ingedrukt, dan status van actieve entity toggelen
                panel = panels[ panel_index ];
                entity_type = panel.entity_id.substr(0, panel.entity_id.indexOf('.'));

                if( ['group', 'light', 'switch'].indexOf( entity_type ) >= 0 )
                {
                    // Hass API-request opbouwen en uitvoeren
                    data = JSON.stringify( { entity_id: panel.entity_id } );

                    request = {
                            host: hass_host,
                            port: hass_port,
                            path: '/api/services/homeassistant/toggle',
                            method: 'POST',
                            headers: { 
                                    "Authorization"  : "Bearer " + hass_token,
                                    "Content-Type"   : "application/json",
                                    "Content-Length" : data.length,
                                },
                        };
                  
                    http.request( request, function(res) {

                        res.on('data', function (d) {
                            // Zonder data-handler loopt de buffer vol
                        });

                        res.on('end', function () {
                            // Entity is geupdate: beeld met nieuwe status opbouwen
                            if( cycle_itv > 0 )
                            {
                                cycle_stop();
                                render_panel();
                                cycle_start();                              
                            } else
                            {
                                render_panel();
                            }
                        });
                    }).end( data );
                } else
                {
                    // Type entity ondersteunt geen toggle: foutmelding
                    cycle_error();
                }
            }
        }
    },
    pin_sw_toggle,
    { debounce: 25, edge: 'both', repeat: true }
);

// De hele zwik opstarten na het booten
E.on('init', function () {
    cycle_start();
});


Samenwerking gezocht!
Deze controller heeft weliswaar het formaat van een luciferdoosje, maar nog geen daadwerkelijk doosje! Heb jij een 3d-printer, en vind je het leuk om een passend ontwerp te maken? Geef dan even een gil: ik stuur je graag de componenten toe om dit project na te maken, in ruil voor een extra afdrukje van jouw gave behuizing :)

Volgende: ESP8266 deep-sleep met Espruino 12-04 ESP8266 deep-sleep met Espruino
Volgende: Espruino firmware @ ESP8266 flashen 04-03 Espruino firmware @ ESP8266 flashen

Reacties


Door Tweakers user Kraz, maandag 25 maart 2019 10:21

Tof gedaan!

Ben zelf de afgelopen week ook bezig geweest met een Wemos D1 mini + DHT22 sensor en een OLED schermpje, maar het wil mij nog niet echt lukken.

Met ESPEasy erop wordt de sensor niet herkend en de tekst op het OLED schermpje valt voor de helft buiten beeld.

Door Tweakers user Thijsmans, maandag 25 maart 2019 10:35

Ik heb geen ervaring ESPEasy, maar je zou even moeten kijken van welke schermgrootte de library uitgaat. De Espruino-library gaat bijvoorbeeld uit van een scherm van 128 pixels breed. Dat is op zich geen punt, want bij deze library zou wat overblijft volgens mij gewoon buiten beeld vallen. Het was wel even wat verschillende scenario's proberen om de positionering uit te vogelen. Misschien een goede reden om eens Espruino te gebruiken? ;)

Die DHT22 zou je eens op een andere ESP moeten aansluiten, zo nodig met dupont-kabeltjes. Dan weet je of het aan de sensor ligt, of bijvoorbeeld aan je gebruikte pins.

Door Tweakers user Kraz, maandag 25 maart 2019 12:06

Thijsmans schreef op maandag 25 maart 2019 @ 10:35:
Ik heb geen ervaring ESPEasy, maar je zou even moeten kijken van welke schermgrootte de library uitgaat. De Espruino-library gaat bijvoorbeeld uit van een scherm van 128 pixels breed. Dat is op zich geen punt, want bij deze library zou wat overblijft volgens mij gewoon buiten beeld vallen. Het was wel even wat verschillende scenario's proberen om de positionering uit te vogelen. Misschien een goede reden om eens Espruino te gebruiken? ;)

Die DHT22 zou je eens op een andere ESP moeten aansluiten, zo nodig met dupont-kabeltjes. Dan weet je of het aan de sensor ligt, of bijvoorbeeld aan je gebruikte pins.
Het lijkt er juist op dat alle data van het scherm te ver naar links staat.
Ik zie dus alleen de rechter kant van de tekst.

Vwb de DHT22 sensor. Ik heb deze met shield en al gekocht en moet dus even kijken of ik losse pinnetjes kan gebruiken. Ik had zelf gehoopt op 'prik-and-play; :D

Door Tweakers user Thijsmans, maandag 25 maart 2019 12:21

Kraz schreef op maandag 25 maart 2019 @ 12:06:
Het lijkt er juist op dat alle data van het scherm te ver naar links staat. Ik zie dus alleen de rechter kant van de tekst.
Zelf een functie schrijven die de breedte van de string berekent, en die aftrekken van de breedte waarvan de library uitgaat?
Vwb de DHT22 sensor. Ik heb deze met shield en al gekocht en moet dus even kijken of ik losse pinnetjes kan gebruiken. Ik had zelf gehoopt op 'prik-and-play; :D
Die kun je zeker gebruiken. De DHT22-shield gebruikt de 3,3V-pin, GND-pin en D4 (= GPIO2) voor data. De rest van de pins is niet aangesloten.

Het probleem kan natuurlijk ook zijn dat je in de code een verkeerd pinnummer hebt gebruikt en dus geen data ontvangt.

Door Tweakers user Worldwide, maandag 25 maart 2019 14:11

Gaaf project!

Is dit een standaard shield met die 3 knoppen?

Ik heb een 3d printer, alleen kan ik niet ontwerpen.
Ik haal mij designs meestal van Thingiverse.com

Daar vind ik dit shield niet trouwens...

Door Tweakers user Thijsmans, maandag 25 maart 2019 16:09

Nee, de tactile switches zitten op een stukje prototype-printplaat (perf-board) dat niet verder is bevestigd aan de D1 en/of oled-shield :)

Door Tweakers user ProAce, maandag 25 maart 2019 16:23

@Thijsmans ik heb een 3D printer en de skills om een behuizingkje er om heen te maken. Stuur maar een berichtje

Door Tweakers user omit94, dinsdag 26 maart 2019 14:38

@Kraz Ik heb zelf ook heel lang lopen klooien met een Wemos D1 Mini + DHT22, op een of andere manier lukte het me niet om de sensor betrouwbaar uit te lezen (soms deed hij het maar meestal gaf hij NaN aan). Dezelfde sensor werkte met een Arduino overigens prima. Als je Googled kom je meerdere mensen met dat probleem tegen. Ik heb het opgelost door een SHT30 shield te gebruiken, die werkt prima.

Door Tweakers user databeestje, woensdag 27 maart 2019 15:29

Ik heb op Thingiverse een dingetje staan die ik gebruik met de ESP32 en ESP8266 NodeMCU v3 bordjes.

https://www.thingiverse.com/thing:3316521

Het is OpenScad, dus verschalen is geen probleem, ik kan er ook wel eentje printen en opsturen.

Reactie formulier
(verplicht)
(verplicht, maar wordt niet getoond)
(optioneel)