My Digital Garden

JPNAS.NET / Digital Garden

🌳 EvergreenLast updated:

Network Door Opener

Electrical, Solar, Wind or some other Energy driven Network Door Opener, with eWeLink, Home Assistant or your own MQTT server and app

Open in the interactive Digital Garden

Minimal Components
Energy Source
Chip that can communicate through WiFi
Relay (electrically operated switch)
Server / Broker
Computer, smartphone, smartwatch, smart car, or any other smart device.
Electronic lock or any type of device suitable for the purpose.

Jump to Examples

Energy Source

The door strike I'm using in the examples below draws about 660 mA at 6 V DC and about 1300 mA at 12 V DC (both stabilised), according to its datasheet. The Sonoff module I'm using in the examples below has an average current consumption of 80mA. The most convenient way would be to use the electricity present in most homes. You would just need to convert AC to DC. You can buy an AC to DC converter for as little as 3 EUR at the time of this writing. Using a solar panel is, at the time of this writing, a very popular choice. Because in most places on Earth, the sun isn't strong enough 24 hours a day, you would also need a battery. As you need to handle the charging of the battery, you either need a charger module or something similar to what I'm using in the examples below – a Solar Power Management Module. The Solar Power Management Module combines several elements. If you choose, you could buy them separately and combine them yourself. For those interested in combining the core elements: you could copy some known designs quite easily with the information available on the internet at the time of this writing. There are also plenty of resources that offer the knowledge necessary to design them autonomously. You could also convert some other forms of energy, like kinetic energy into electrical energy. Many individuals around the world are trying to find methods to convert energy so it would be usable for our needs.

Some open-source tools:

For this purpose, we will use a commercially available mini turbine. Priyabrata Adhikary, Pankaj Kr Roy, and Asis Mazumdar from Jadavpur University, Kolkata, India, conducted a case study. I also did some basic measurements myself. According to the mentioned case study, the technical documentation for the turbine, and my measurements, the minimum pressure is about 0.05 MPa. The International System of Units states that, in terms of units, Pa = m⁻¹·kg·s⁻². For some people, it is easier to imagine in bar: 0.5 bar is roughly 5 meters of water column (mH₂O). It is clear that we either need to ensure a relatively high elevation to store the water or collect large amounts of it. We are going to use online tools to calculate how much water is needed.

Example roof:

House height (gutter): 4 m
Roof surface area: 200 m²

With the help of online community tools, we get an amount of roughly 1 m³ of water per day for the module. During a 1-week period with no rain, we would still need to "collect" 7 m³ of water for the week just for the module. With a 200 m² roof, we would need a minimum annual water volume of 7 m³ × 52 weeks = 364 m³ (equivalent to an annual precipitation depth of 1820 mm) for only the modules. Because water "gains" energy that can be "collected" during free fall in the atmosphere, anyone trying to "collect" more energy with a higher gutter height would need an accordingly taller setup. Collecting these amounts does not make sense, even when considering the evaporation effect. To "collect" or store energy in some other form requires additional resources and, as it does not rain continuously at the same rate, poses another challenge. The students' work from Worcester Polytechnic Institute (WPI) offers quite interesting insights into this topic.

Wind is far less dense. With the help of online tools, we can determine the surface area needed to power the module and the strike. Make sure to "realistically" input the efficiency of the turbine. You also have to find a decent product for the turbine. Many companies offer mini wind turbines, but sadly, in my experience, only a few are actually useful. Energy harvesting is easier today than ever before—or rather, in known history. But when you transition from marketing promises to physical reality, you find that there are not many truly useful products available. Even if you think you are doing good for the environment, there is a broader concept that is not as easy to interpret as one might think. The first thing to consider is: How Green is 'Green' Energy? What resources and how much energy are needed to produce a wind turbine versus how much energy you can extract from it over its lifespan should be the first question to ask if you are considering buying a wind turbine to "help" the environment.

Some thoughts to consider:

Atomic Energy:

To choose the “right” energy source isn't as easy as one might think. There is so much controversial research that it's hard to really know the “truth”. If you're not concerned about all the problems on how humanity is using energy and resources, then it's just a matter of choosing a product that fits the bill. It's basically more or less a question of how pessimistic or optimistic your worldview is and what obligations you think we have to future generations.

Chip Capable of Executing 1 Command and Communicating Through a Network

You could make a chip yourself, but it's far from easy. Since you can buy a module that features the ESP8285 chip—suitable for this purpose—for less than $3 at the time of writing, trying to build one yourself would be a waste of time and money. Attempting to build one for other reasons would only be feasible by investing a significant amount of time and resources. If size is not a constraint, you must consider that such a device would be complex, and the potential for error increases with every added component. Let’s explore what it would take to build such a device.

The modules used in the examples below can maintain state: after activation, they can remain on for a second before switching off, or stay on for a preset duration. Implementing this in a stationary setup typically involves using a so-called relay (an electrically operated switch). The chip sends a control signal, while the relay handles the switching based on the logic configured in the device's firmware. To send this signal, an existing device is used, so we first need to understand how these devices communicate. For this type of communication, the MQTT protocol is currently the most widely used. The MQTT protocol follows a client–server model. The device we are designing "only" needs to be able to communicate with the server / broker. To communicate with an MQTT broker over Wi-Fi, a Wi-Fi connection must also be established. How do existing devices communicate over Wi-Fi? The IEEE 802.11 standard is standardly used today. How many bits are required for IEEE 802.11? How many bits for MQTT? If you speak English or German, numerous resources are available for learning about hardware implementation. It all occurs at the binary level—at least on deterministic machines. Nondeterministic machines, on the other hand, apply a fundamentally different approach.

Quantum computing:

Most modern devices operate using Boolean logic. Binary implementation is currently achieved through integrated circuits on semiconductor chips. Anyone interested could build logic gates using discrete transistors; many developers have shared their implementations online. It is worth noting that according to this source, calculator logic gate density grew from 12 gates in the 1960s to over 1,000 in the early 1970s. Today, world-leading companies continue to advance Wi-Fi chipsets.

Some resources:

Server / Broker

Due to the nature of this communication, a central device is used to manage and route messages to and from the chips. The so-called MQTT protocol is currently the most widely used standard for this type of communication.

Resources and tools:

Examples

Working with electricity can be dangerous and can result in serious injury or death! Most countries have laws that prohibit uncertified persons from working with electricity. Based on my knowledge, some minor work is allowed in most countries, but you should carefully check the regulations in your country and consult a professional. Should you decide to act upon any information on this website, you do so at your own risk!

All five examples open the same door with the same electric strike (effeff 128.14 D11). According to its datasheet it is fail-locked (the door stays locked without power), accepts 6–12 V AC/DC, draws about 660 mA at 6 V DC and about 1300 mA at 12 V DC (both stabilised), and is specified for −15 °C to +40 °C. At 6 V it can only release a latch that presses against it with up to 10 N, at 12 V up to 60 N, so 12 V is the safer choice if the door sits under load. The SONOFF RE5V1C is a 5 V module with a dry-contact relay output (10 A), built around the ESP8285 and specified for 0–40 °C. It is a 2.4 GHz Wi-Fi device only.

Overview

#EnergyControl pathServerPhone / client
1Mains → 12 V DCeWeLink (cloud + LAN)eWeLink cloudeWeLink app
25–10 W solar panel + Li-ioneWeLink (cloud + LAN)eWeLink cloudeWeLink app
3Mini wind turbine + 12 V batteryeWeLink (cloud + LAN)eWeLink cloudeWeLink app
420 W solar panel + 12 V batteryESPHome native APIHome Assistant (own server)Home Assistant Companion app
520 W solar panel + 12 V batteryMQTT over TLSMosquitto (own server)Own Android / iPhone app

How to read the numbers below: they are order-of-magnitude estimates for central Europe, based on the figures in this note (80 mA average for the SONOFF module) and on typical values for batteries and panels. Measure your own consumption with a USB power meter and look up the solar radiation for your location in PVGIS before you buy anything. One door release costs almost nothing: 6 V × 0.66 A × 1 s ≈ 0.001 Wh (12 V × 1.3 A × 1 s ≈ 0.004 Wh). What drains the battery is the Wi-Fi module that has to stay connected 24 hours a day.

Example 1 – Mains powered, SONOFF + eWeLink

No-brainer application guide.

Wiring and settings:

  1. Use a ready-made, CE-marked, enclosed supply. Everything behind it is low voltage (12 V / 5 V). Do not work on bare mains wiring yourself.
  2. 12 V+ → fuse → relay dry contact → strike (+). Strike (−) → 12 V GND. The relay only switches; it does not power anything.
  3. Put the diode directly across the strike terminals, stripe (cathode) towards +. It absorbs the voltage spike when the strike coil switches off and protects the relay contact. DC strikes only; never on an AC supply.
  4. Adjust the buck converter to 5.0 V with a multimeter before connecting the module. Many adjustable converters ship set to a much higher voltage.
  5. In eWeLink, set the module to inching mode with 1 s, so the strike is never energised longer than necessary, even if the phone loses connection.
  6. Give the module a fixed DHCP lease in your router and test the door once with the Internet cable unplugged: the module also supports LAN control without Internet access.
  7. Mount the electronics indoors. The module is specified for 0–40 °C.

Example 2 – Solar powered, SONOFF + eWeLink

Energy budget (estimates)

SONOFF module, 80 mA at 5 V, 24 h≈ 9.6 Wh per day
5 W panel in December (about 1 full-sun hour, 70 % system efficiency)≈ 3.5 Wh per day
5 W panel in June (about 5 full-sun hours)≈ 17 Wh per day
One 14500 cell (about 800 mAh, 3.7 V), usable after conversion losses≈ 2 Wh
Three 14500 cells in parallel≈ 6 Wh, which is only about 15 hours without sun
Two 18650 cells (about 3000 mAh each)≈ 22 Wh usable

What this means: with the 80 mA module a 5 W panel works from spring to autumn, but in winter it harvests clearly less than the module uses, and three 14500 cells only bridge a night. The panel can charge two or even three 14500 cells in parallel, but that says little about the winter balance. There are three ways to fix this, and they can be combined:

  1. Reduce the consumption. The RE5V1C is ESP8285 based and can be flashed with ESPHome (see Example 4), which lets you set the Wi-Fi power-saving mode. I would expect a value well below 80 mA, perhaps around 25 mA, which would be about 3 Wh per day. A higher power-saving level adds some latency to the command, so measure both consumption and response time.
  2. Raise the energy storage and harvest. Use 18650 cells, a 10 W panel, and mount it at about 60–70° facing south for the winter months, free of shade. Use at least 1 mm² cable between panel and module.
  3. Keep the battery warm. Do not charge Li-ion cells below 0 °C. Only the panel belongs outside; the battery and electronics go inside the house, near the door, behind the secure side of the door frame. If you must have the battery outside, check that the module has a low-temperature charge cut-off, or use LiFePO4 cells with a suitable BMS.

Also check the peak current: a 14500 cell has little reserve for the roughly 1.3 A pulse at the battery that a 6 V strike causes through a boost converter. Use good cells, or 18650 cells. How long it will operate without sunlight and how many times you can actuate the door depends heavily on the battery capacity. You can calculate this manually or use an online battery life calculator. To estimate charge duration, consult a Battery Charging Time Calculator.

Example 3 – Wind turbine, SONOFF + eWeLink

Wind is a good partner to solar in winter, when the sun is weak, but a gutter-height mini turbine will disappoint you. Power grows with the cube of the wind speed: P = ½ · ρ · A · v³ · Cp, where ρ ≈ 1.225 kg/m³ is the air density, A the rotor area and Cp the overall efficiency (the theoretical maximum is the Betz limit of 59 %; small turbines including generator and charger reach roughly 0.15–0.25). Below the cut-in speed (often 2.5–3 m/s) the turbine produces nothing at all.

Rotor diameter 0.6 m (A ≈ 0.28 m²), Cp ≈ 0.2, Rayleigh wind distribution, estimates

Annual mean wind speed at the rotorEnergy per day
2 m/s≈ 4 Wh
3 m/s≈ 20 Wh
4 m/s≈ 70 Wh

The module with the stock firmware needs about 10.7 Wh per day from a 12 V system (80 mA at 5 V plus buck converter losses), so you need a site with a mean wind speed of about 3 m/s or more, and a rotor of at least 0.6 m. With a power-saving ESPHome firmware (Example 4) the need drops to about 4 Wh per day. As a rule of thumb, a turbine should stand about 9 m above any obstacle within roughly 90 m; on a roof, turbulence cuts the yield and shakes the building. Measure the wind at the planned position for a few weeks with a cheap anemometer before you buy the turbine, and check whether you need a building permit.

  • Small 12 V three-phase wind turbine, rotor diameter 0.6 m or more
  • 12 V wind/solar hybrid charge controller with a dump load or brake function (the controller rectifies the three-phase AC itself)
  • 12 V battery: a 7 Ah AGM battery stores about 84 Wh nominal (about 42 Wh usable), which is roughly four calm days at 10.7 Wh per day
  • 12 V → 5 V step-down converter with a low idle current (below 1 mA). Cheap LM2596 boards typically waste about 5 mA, which is more than 1 Wh per day
  • SONOFF 5V Wi-Fi Inching/Self-Locking Relay Module
  • Electric Strike, supplied directly from the 12 V battery through the relay contact
  • 2–3 A fuse next to the battery, diode across the strike as in Example 1
  • eWeLink App

Important:

  • Never connect a turbine directly to a converter or a Li-ion charger. In a storm an unloaded turbine reaches a multiple of its rated voltage and destroys everything behind it. Use a controller that is made for wind turbines and keep the dump load in place.
  • Stop a turbine mechanically (or short its phases through the controller's brake) before you work on it.
  • A solar panel can be connected to the same hybrid controller if it has a solar input. The two sources complement each other in winter.
  • The life-cycle questions from the energy source section apply here as well: a turbine of this size has to run for years to repay the energy used to produce it.

Example 4 – Solar panel, Home Assistant on your own server

Instead of the eWeLink cloud, the door is controlled by Home Assistant running at home. The SONOFF module stays, but is flashed with ESPHome. Home Assistant talks to the device directly over the local network with the encrypted ESPHome API. No cloud account is needed, and the door still opens when the Internet is down.

  • Server: Raspberry Pi, mini PC or virtual machine running Home Assistant OS (with a fixed IP address)
  • 20 W / 12 V solar panel
  • 12 V solar charge controller (PWM is enough for this size; MPPT gains a bit in winter)
  • 12 V battery (7 Ah AGM or a small LiFePO4 battery with BMS), kept indoors
  • Low-idle-current 12 V → 5 V step-down converter
  • SONOFF RE5V1C, flashed with ESPHome
  • Electric Strike at 12 V, diode, 2–3 A fuse
  • 3.3 V USB-UART adapter for the first flashing

Energy: a 20 W panel gives about 14 Wh per day in December (1 full-sun hour, 70 % efficiency) against about 10.7 Wh needed with the stock-firmware consumption, and far more than the roughly 4 Wh needed with power saving. An 84 Wh battery bridges several days without sun.

Setup:

  1. Install Home Assistant and add the ESPHome add-on (or run ESPHome in Docker).
  2. Flash the RE5V1C on the bench: connect the UART adapter to the ERX/ETX pads (see the device page), install the configuration below once over the cable, and later updates over Wi-Fi. This replaces the stock firmware, and you cannot easily go back to the eWeLink firmware.
  3. Home Assistant discovers the device. Add it, and put the "Open door" button on a dashboard.
  4. Create a separate, non-administrator Home Assistant user for each family member. The Companion app for Android and iPhone is your phone client.
  5. For access from outside, use a VPN such as WireGuard or Home Assistant Cloud. Do not publish port 8123 on the Internet without a reverse proxy, TLS and two-factor authentication.
# door-opener.yaml (ESPHome, SONOFF RE5V1C)
esphome:
  name: door-opener

esp8266:
  board: esp8285

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: light      # "high" saves more, but adds latency

api:
  encryption:
    key: !secret api_key

ota:
  - platform: esphome
    password: !secret ota_password

logger:
  baud_rate: 0                # keep the UART pads free

switch:
  - platform: gpio
    id: strike
    pin: GPIO12               # relay on the RE5V1C
    restore_mode: ALWAYS_OFF
    internal: true
    on_turn_on:               # safety: never stay on for longer than 5 s
      - delay: 5s
      - switch.turn_off: strike

button:
  - platform: template
    name: "Open door"
    on_press:
      - switch.turn_on: strike
      - delay: 1s
      - switch.turn_off: strike

sensor:
  - platform: wifi_signal
    name: "Door opener Wi-Fi signal"
    update_interval: 60s
  - platform: uptime
    name: "Door opener uptime"

Because Home Assistant keeps a log, you also get a free history of who opened the door and when, and you can add automations, for example a notification to your phone each time the door is released.

Example 5 – Solar panel, MQTT on your own server, own Android and iPhone app

Here there is no ready-made platform in between: the door controller, your Mosquitto broker and your own phone app speak MQTT with each other, protected with TLS and per-user passwords. This is the minimal-components idea from the top of this note, taken all the way to the phone.

  • Server: any Linux machine at home or in a data centre with a public host name, running Eclipse Mosquitto
  • Door controller: ESP32 development board and a 5 V relay module (opto-isolated). I use an ESP32 here instead of the ESP8285, because the ESP32 can verify a TLS certificate authority and can measure the battery voltage
  • The same power block as in Example 4 (20 W / 12 V solar panel, controller, 12 V battery, low-idle-current 12 V → 5 V converter, fuse, diode)
  • Electric Strike at 12 V through the relay contact
  • Voltage divider 100 kΩ / 22 kΩ from the battery to an ADC pin of the ESP32 (battery voltage monitoring)
  • The phone app (source code below)

Energy: an ESP32 with Wi-Fi power saving typically needs 20–40 mA at 3.3 V, so about 2–4 Wh per day plus converter losses. The power block of Example 4 covers this comfortably.

Topics:

  • door/entrance/cmd: the phone publishes OPEN (QoS 1, never retained, otherwise the door would open again every time the controller reconnects)
  • door/entrance/event: the controller publishes opened after the relay cycle, so the app knows that the door was really released, not only that the broker accepted the message
  • door/entrance/status: online / offline (retained, offline is published by the broker as last will)
  • door/entrance/sensor/battery_voltage/state: battery voltage

1. Broker (configuration reference). Get a certificate, for example with Let's Encrypt, and make it readable for the mosquitto user:

# /etc/mosquitto/conf.d/door.conf
listener 8883
cafile /etc/ssl/certs/ca-certificates.crt
certfile /etc/letsencrypt/live/mqtt.example.com/fullchain.pem
keyfile /etc/letsencrypt/live/mqtt.example.com/privkey.pem
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

# create users (the first one with -c)
# sudo mosquitto_passwd -c /etc/mosquitto/passwd door_device
# sudo mosquitto_passwd /etc/mosquitto/passwd phone_ana

# /etc/mosquitto/acl
user door_device
topic readwrite door/entrance/#

user phone_ana
topic write door/entrance/cmd
topic read door/entrance/#

Open only port 8883 in the firewall. One account per phone means that you can lock out a lost phone by deleting a single line. Test it first from a computer: mosquitto_pub -h mqtt.example.com -p 8883 --cafile /etc/ssl/certs/ca-certificates.crt -u phone_ana -P PASSWORD -t door/entrance/cmd -m OPEN

2. Door controller (ESPHome MQTT component). Check the pin numbers, the relay polarity (inverted: true for active-low boards) and the ADC options for your ESPHome version:

# door-opener-mqtt.yaml (ESPHome, ESP32)
esphome:
  name: door-opener-mqtt

esp32:
  board: esp32dev
  framework:
    type: esp-idf

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: light

ota:
  - platform: esphome
    password: !secret ota_password

mqtt:
  broker: mqtt.example.com
  port: 8883
  username: !secret mqtt_user
  password: !secret mqtt_pass
  certificate_authority: !secret mqtt_ca   # PEM text of the CA
  topic_prefix: door/entrance
  discovery: false
  log_topic: null
  on_message:
    - topic: door/entrance/cmd
      payload: "OPEN"
      qos: 1
      then:
        - switch.turn_on: strike
        - delay: 1s
        - switch.turn_off: strike
        - mqtt.publish:
            topic: door/entrance/event
            payload: "opened"
            qos: 1

switch:
  - platform: gpio
    id: strike
    pin: GPIO26
    restore_mode: ALWAYS_OFF
    internal: true
    on_turn_on:               # safety: never stay on for longer than 5 s
      - delay: 5s
      - switch.turn_off: strike

sensor:
  - platform: adc
    pin: GPIO34
    name: "Battery voltage"
    update_interval: 60s
    attenuation: 12db
    filters:
      - multiply: 5.545       # divider 100k / 22k: (100 + 22) / 22

The default topic names of the battery sensor and the status message follow from topic_prefix, which is why they match the list above. A Let's Encrypt certificate is renewed every 90 days; the controller trusts the certificate authority and not a single certificate fingerprint, so renewal does not break it.

3. Phone app (Android and iPhone). The source is a small Flutter project (one codebase for both systems) (full source below). It stores the server data in the secure storage of the phone (Android Keystore / iOS Keychain), connects with TLS on port 8883, shows whether the controller is online and the battery voltage, and opens the door only on a long press (so a touch in a pocket does nothing). The core is:

final client = MqttServerClient.withPort(host, clientId, 8883)
  ..secure = true
  ..keepAlivePeriod = 30
  ..autoReconnect = true
  ..resubscribeOnAutoReconnect = true;
await client.connect(user, password);

client.subscribe('door/entrance/status', MqttQos.atLeastOnce);
client.subscribe('door/entrance/event', MqttQos.atLeastOnce);

final payload = MqttClientPayloadBuilder()..addString('OPEN');
client.publishMessage('door/entrance/cmd', MqttQos.atLeastOnce,
    payload.payload!, retain: false);
  1. Install Flutter, run flutter create door_opener, then flutter pub add mqtt_client flutter_secure_storage, and replace lib/main.dart with the lib/main.dart source below.
  2. Android: add <uses-permission android:name="android.permission.INTERNET"/> to android/app/src/main/AndroidManifest.xml (release builds need it) and set minSdkVersion to 23 if the build asks for it. Build with flutter build apk and install the file on your phone.
  3. iPhone: building needs a Mac with Xcode. A free Apple ID lets you install the app on your own phone for 7 days at a time; a paid developer account is needed for longer validity, TestFlight or the App Store.
  4. On first start tap the gear icon and enter the host name, port 8883, user name and password of that phone's account.

If you do not want to write an app, ready-made MQTT dashboard apps for Android and iOS (for example IoT MQTT Panel) can publish OPEN to door/entrance/cmd with a button. The own app is nicer, because it checks the controller status and confirms the release.

Next steps for the app: biometric confirmation before opening (the local_auth package), notifications when the door is opened, a widget on the home screen. Resources: mqtt_client package, flutter_secure_storage package.

Full source: lib/main.dart

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';

const _base = 'door/entrance';
const _store = FlutterSecureStorage();

void main() => runApp(const DoorApp());

class DoorApp extends StatelessWidget {
  const DoorApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'Door opener',
        theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
        darkTheme: ThemeData(
          colorSchemeSeed: Colors.teal,
          useMaterial3: true,
          brightness: Brightness.dark,
        ),
        home: const DoorPage(),
      );
}

class DoorPage extends StatefulWidget {
  const DoorPage({super.key});

  @override
  State<DoorPage> createState() => _DoorPageState();
}

class _DoorPageState extends State<DoorPage> {
  MqttServerClient? _client;
  StreamSubscription? _sub;
  String _link = 'starting'; // phone <-> broker
  String _device = 'unknown'; // door controller: online / offline
  String? _battery;

  @override
  void initState() {
    super.initState();
    _connect();
  }

  @override
  void dispose() {
    _sub?.cancel();
    _client?.disconnect();
    super.dispose();
  }

  void _set(VoidCallback f) {
    if (mounted) setState(f);
  }

  void _toast(String msg) {
    if (!mounted) return;
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(msg)));
  }

  Future<void> _connect() async {
    final cfg = await _store.readAll();
    final host = cfg['host'];
    final user = cfg['user'];
    final pass = cfg['pass'];
    final port = int.tryParse(cfg['port'] ?? '') ?? 8883;
    if (host == null || host.isEmpty || user == null || pass == null) {
      _set(() => _link = 'not configured');
      return;
    }

    await _sub?.cancel();
    _client?.disconnect();

    final id = 'door-app-${DateTime.now().millisecondsSinceEpoch}';
    final c = MqttServerClient.withPort(host, id, port)
      ..secure = true
      ..keepAlivePeriod = 30
      ..autoReconnect = true
      ..resubscribeOnAutoReconnect = true
      ..logging(on: false)
      ..onConnected = (() => _set(() => _link = 'connected'))
      ..onDisconnected = (() => _set(() => _link = 'disconnected'))
      ..onAutoReconnect = (() => _set(() => _link = 'reconnecting'))
      ..onAutoReconnected = (() => _set(() => _link = 'connected'));
    c.connectionMessage =
        MqttConnectMessage().withClientIdentifier(id).startClean();

    _set(() {
      _client = c;
      _link = 'connecting';
      _device = 'unknown';
      _battery = null;
    });

    try {
      await c.connect(user, pass);
    } catch (_) {
      c.disconnect();
      _set(() => _link = 'error');
      return;
    }
    if (c.connectionStatus?.state != MqttConnectionState.connected) {
      c.disconnect();
      _set(() => _link = 'error');
      return;
    }

    c.subscribe('$_base/status', MqttQos.atLeastOnce);
    c.subscribe('$_base/event', MqttQos.atLeastOnce);
    c.subscribe('$_base/sensor/battery_voltage/state', MqttQos.atLeastOnce);

    _sub = c.updates!.listen((events) {
      for (final e in events) {
        final msg = e.payload as MqttPublishMessage;
        final text =
            MqttPublishPayload.bytesToStringAsString(msg.payload.message);
        if (e.topic == '$_base/status') {
          _set(() => _device = text);
        } else if (e.topic == '$_base/sensor/battery_voltage/state') {
          final v = double.tryParse(text);
          _set(() => _battery = v?.toStringAsFixed(2));
        } else if (e.topic == '$_base/event' && text == 'opened') {
          HapticFeedback.mediumImpact();
          _toast('Door released');
        }
      }
    });
  }

  void _open() {
    final c = _client;
    if (c == null ||
        c.connectionStatus?.state != MqttConnectionState.connected) {
      _toast('Not connected to the server');
      return;
    }
    if (_device != 'online') {
      _toast('Door controller is not online');
      return;
    }
    HapticFeedback.heavyImpact();
    final payload = MqttClientPayloadBuilder()..addString('OPEN');
    // retain must stay false, otherwise the door would open again on every reconnect
    c.publishMessage('$_base/cmd', MqttQos.atLeastOnce, payload.payload!,
        retain: false);
    _toast('Command sent');
  }

  Future<void> _settings() async {
    final cfg = await _store.readAll();
    final host = TextEditingController(text: cfg['host'] ?? '');
    final port = TextEditingController(text: cfg['port'] ?? '8883');
    final user = TextEditingController(text: cfg['user'] ?? '');
    final pass = TextEditingController(text: cfg['pass'] ?? '');
    if (!mounted) return;

    final save = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Server'),
        content: SingleChildScrollView(
          child: Column(mainAxisSize: MainAxisSize.min, children: [
            TextField(
              controller: host,
              decoration: const InputDecoration(labelText: 'Host'),
              keyboardType: TextInputType.url,
              autocorrect: false,
            ),
            TextField(
              controller: port,
              decoration: const InputDecoration(labelText: 'Port (TLS)'),
              keyboardType: TextInputType.number,
            ),
            TextField(
              controller: user,
              decoration: const InputDecoration(labelText: 'User'),
              autocorrect: false,
            ),
            TextField(
              controller: pass,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
            ),
          ]),
        ),
        actions: [
          TextButton(
              onPressed: () => Navigator.pop(ctx, false),
              child: const Text('Cancel')),
          FilledButton(
              onPressed: () => Navigator.pop(ctx, true),
              child: const Text('Save')),
        ],
      ),
    );

    if (save == true) {
      await _store.write(key: 'host', value: host.text.trim());
      await _store.write(key: 'port', value: port.text.trim());
      await _store.write(key: 'user', value: user.text.trim());
      await _store.write(key: 'pass', value: pass.text);
      await _connect();
    }
  }

  @override
  Widget build(BuildContext context) {
    final linked = _link == 'connected';
    final online = _device == 'online';
    return Scaffold(
      appBar: AppBar(
        title: const Text('Door'),
        actions: [
          IconButton(
              icon: const Icon(Icons.settings), onPressed: _settings),
        ],
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Wrap(spacing: 8, runSpacing: 8, children: [
                Chip(
                  avatar: Icon(linked ? Icons.cloud_done : Icons.cloud_off,
                      size: 18),
                  label: Text('Server: $_link'),
                ),
                Chip(
                  avatar: Icon(
                      online ? Icons.sensors : Icons.sensors_off,
                      size: 18),
                  label: Text('Controller: $_device'),
                ),
                if (_battery != null)
                  Chip(
                    avatar: const Icon(Icons.battery_full, size: 18),
                    label: Text('$_battery V'),
                  ),
              ]),
              const SizedBox(height: 48),
              SizedBox(
                width: 220,
                height: 220,
                child: FilledButton(
                  style: FilledButton.styleFrom(shape: const CircleBorder()),
                  onPressed: () => _toast('Hold the button to open'),
                  onLongPress: (linked && online) ? _open : null,
                  child: const Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(Icons.lock_open, size: 64),
                      SizedBox(height: 8),
                      Text('Hold to open'),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

README.md

# Door opener app (Android + iPhone)

Small Flutter app that publishes `OPEN` to `door/entrance/cmd` on your own Mosquitto broker (TLS, port 8883).
Not tested on hardware: build it, test with the door strike disconnected first.

## Build

```bash
flutter create door_opener
cd door_opener
flutter pub add mqtt_client flutter_secure_storage
# replace lib/main.dart with the file from this folder
```

**Android**
- Add to `android/app/src/main/AndroidManifest.xml` (inside `<manifest>`):
  `<uses-permission android:name="android.permission.INTERNET"/>`
- If the build complains, set `minSdkVersion` to 23 (needed by flutter_secure_storage).
- `flutter build apk` and install the APK.

**iPhone**
- Needs a Mac with Xcode. `flutter run` with the phone connected, or open `ios/Runner.xcworkspace`.
- A free Apple ID signs the app for 7 days; a paid developer account is needed for longer validity, TestFlight or App Store.

## Topics

| Topic | Direction | Content |
|---|---|---|
| `door/entrance/cmd` | app → controller | `OPEN` (QoS 1, never retained) |
| `door/entrance/event` | controller → app | `opened` |
| `door/entrance/status` | controller → app | `online` / `offline` |
| `door/entrance/sensor/battery_voltage/state` | controller → app | battery voltage |

For all examples

  • Keep the mechanical key. The strike is fail-locked, so an empty battery means a locked door, not an open one.
  • Everything that can release the strike (controller, relay, battery and the strike wiring) belongs on the secure side of the door. Only the panel or the turbine, which cannot open anything, should be outdoors. Otherwise the wires at the strike are an invitation to bridge them.
  • Use unique, long passwords, keep the firmware and the server updated, and never expose a door-control interface to the Internet without TLS and authentication.
  • Make sure the strike is never energised for longer than its datasheet allows. The examples limit it to 1 s with a 5 s safety limit in the firmware.
  • If the door is a fire or escape route, check the regulations: such doors have strict requirements, and the strike used here is not suitable for fire doors.

Some resources:

← All notes