Written by: Jörg Wendel (linux at jwendel dot de) Project page: https://github.com/horchi/linux-homectld
This code is distributed under the terms and conditions of the GNU GENERAL PUBLIC LICENSE. See the file LICENSE for details.
If you like the project and want to support it
- GPIO of Raspberry Pi or Odroid (tested with Odroid N2+)
- Deconz https://www.dresden-elektronik.de/funk/software/deconz.html
- OpenWeatherMap https://openweathermap.org/
- NodeRed - interface from and to NodeRed
- Growatt PV Inverter via https://github.com/OpenInverterGateway/OpenInverterGateway
- Rtl433 wireless sensors - details below
- One Wire Sensors
- Support of some i2c bus circuits like ADS1115, MCP23017, DHT20 and the TCA9548A multiplexer
- Arduino(s) with analog and digital inputs via MQTT
- Thetford refrigerator via CI bus
- BMS Interface (Battery Management System) like this https://www.lithiumbatterypcb.com/product/4s-lifepo4-battery-bluetooth-bms-with-100a-constant-discharge-current-for-12v-lfp-battery-pack/ as used for example for the LIONTRON LiFePO4
- Supports the RS485 Bus and the 'Simple Serial Display' interface of Votronic
- Supports the VE.Direct-HEX-Protocol-Phoenix-Inverter protocol of Victron devices
- Act as frontend of LMS (Logitech Media Server)
- Act as a remote control of the VDR (Linux Video Disc Recorder)
- Script adapter/converter interface for custom MQTT messages - to be able to integrate almost all sensors that deliver data via MQTT
- Windy App / Windy App Map to display wind data from https://windy.app/de/widgets
- Control of WiFi management via nmcli client integrated in the web interface
- Control of system services via systemd integrated in the web interface
- Display and filtering of rsyslog logs files integrated in the web interface
- HomeMatic - by using NodeRed as an adapter to the CCU
USE AT YOUR OWN RISK. No warranty. This software is a private contribution and not to any products and can also cause unintended behavior. Use at your own risk! THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The software was created for personal use, it's published here for free under the GPLv2. The construction of the required hardware, especially in the handling of mains voltage (230V, 110V, ...), is carried out on one's own responsibility, local guidelines and regulations must also be observed especially in connection with water and pool! All explanations about the hardware are only indications of how it is technically feasible, security and legal provisions are not discussed here!
The described installation method and dependencies fit to ubuntu 24.04, the homectld work also with other Linux distributions
Language pack 'de_DE.UTF-8' is required as language package:
dpkg-reconfigure locales
dpkg-reconfigure keyboard-configuration
reboot
Set system timezone, for example Berlin
timedatectl set-timezone Europe/Berlin
Change hostname
hostnamectl set-hostname new-name
Update your system
apt update
apt dist-upgrade
Perform all following steps as root user! Either by getting root or by prefix each command with sudo.
To become root type: sudo -i
It's not required to host the database local at the Raspberry. A remote database is supported as well.
apt -y install mariadb-server
or if you like mariadb-server 10.6 on ubuntu 20.04 follow this instructions: https://community.hetzner.com/tutorials/how-to-install-mariadb-10-6-on-ubuntu-20-04
Set the database root (db admin) password during installation!
If the database server is located locally (on same host as the daemon):
> mysql -u root -Dmysql -p
CREATE DATABASE homectl charset utf8;
CREATE USER 'homectl'@'localhost' IDENTIFIED BY 'homectl';
GRANT ALL PRIVILEGES ON homectl.* TO 'homectl'@'localhost' IDENTIFIED BY 'homectl';
flush privileges;
if the database is running remote, or you like to have remote access to the database:
> mysql -u root -Dmysql -p
CREATE DATABASE homectl charset utf8;
CREATE USER 'homectl'@'%' IDENTIFIED BY 'homectl';
GRANT ALL PRIVILEGES ON homectl.* TO 'homectl'@'%' IDENTIFIED BY 'homectl';
flush privileges;
The short way, after Make.user is set up:
make install-dependencies
It installs the packages below via apt, only the missing ones, installed packages are never upgraded.
libgpiod (version 2 needed) and libwebsockets (4.3.0 needed) are built from source if the versions of
the distribution are too old (e.g. Ubuntu 24.04 / Armbian noble ship libgpiod 1.6), the sources go to
SRC_DIR from Make.user (default /usr/src). The optional parts follow the switches of Make.user:
WOMO adds mariadb-server, mosquitto and the network tools (dnsmasq, NetworkManager, chrony, LTE modem,
iptables, openvpn), MOPEKA the python BLE modules, THETFORD paho-mqtt and usblini, GARMIN the python venv,
GPS the python modules of the u-blox GPS service (gps.u-blox, installed with make install like the others).
Always call it via make, the script contrib/install-dependencies.sh gets the switches from there.
DRYRUN=1 make install-dependencies only shows the commands. The manual way:
apt -y install build-essential libssl-dev libcurl4-openssl-dev uuid-dev libcap-dev libsystemd-dev cmake pkg-config net-tools nodejs
apt -y install libjansson-dev libmariadb-dev liblua5.3-dev mosquitto-clients jq jo bc aptitude util-linux-extra libgpiod2 libgpiod-dev
apt -y install libbluetooth-dev
apt -y install mosquitto
Put this lines into your /etc/mosquitto/mosquitto.conf:
listener 1883
allow_anonymous true
and restart mosquitto systemctl restart mosquitto
Mit libgpiod (besser) - zwingend Version 2 !
apt -y install libgpiod-dev
zur Not fallback auf wiringpi
apt -y install wiringpi
Mit libgpiod (besser) - zwingend Version 2 !
apt -y install libgpiod-dev
zur Not fallback auf wiringpi
apt -y install libwiringpi-dev libwiringpi2 odroid-wiringpi
We need to install it manually in case the version shipped with the distribution is to old (we need at least version v4.3.0)
if the version shipped with the distribution is okay simple
apt -y install libwebsockets-dev
else get it from git
cd /usr/src/
git clone https://github.com/warmcat/libwebsockets.git
cd libwebsockets
mkdir build
cd build
cmake ..
make install
ldconfig
cd /usr/src/
git clone https://github.com/horchi/linux-homectld/
cd linux-homectld
make clean all
make install
Now the home control daemon is installed in folder /usr/local/bin
Check /etc/homectld/daemon.conf file for setting of your database login. If you have used the defaults above no change is needed.
To make the sensors available to the Raspberry PI you have to load the w1-gpio module by registering it in /etc/modules
(here in detail : http://www.netzmafia.de/skripten/hardware/RasPi/Projekt-Onewire/index.html)
echo "w1-gpio" >> /etc/modules
echo "w1_therm" >> /etc/modules
echo "dtoverlay=w1-gpio,gpioin=4,pullup=on" >> /boot/config.txt
echo "w1-gpio" >> /etc/modules-load.d/1wire.conf
echo "w1_therm" >> /etc/modules-load.d/1wire.conf
Addd this two lines into the [generic] section of the /boot/config.ini
overlays="w1-gpio"
overlay_param="gpiopin=476"
Reboot to apply this settings!
The homectld checks automatically if there are 'One Wire Sensors' connected, each detected sensor will be configurable via the web interface.
homectld can drive an openHASP touch panel (e.g. the Elecrow CrowPanel 5.0) as a
small wall or vehicle display. The pages of the panel are configured in the WEBIF under
Setup -> HASP Panel, stored in the tables hasppages / hasppagewidgets and sent to the panel via MQTT.
- openHASP firmware on the panel, MQTT configured to the same broker as homectld
(the group topic of the panel is
hasp/platesby default) - configuration item HASP Panel -> haspMqttTopic (
hasp/platesorhasp/<hostname>); empty = disabled - for MDI symbols the complete Material Design Icons font has to be uploaded to the panel once as
/mdi.ttf(the file of the WEBIF:htdocs/mds/fonts/materialdesignicons-webfont.ttf), e.g.curl -F "data=@materialdesignicons-webfont.ttf;filename=/mdi.ttf" http://<panel-ip>/edit, then reboot the panel
- a page has up to 4 rows, each row 1..3 widgets (cards); pages are changed by swiping left/right on the panel
- per slot a sensor is selected (filter and sorting by type / name), Setup opens the widget dialog
- when a sensor is selected its settings are copied once from its dashboard widget (if it is on a dashboard, otherwise the sensor defaults); afterwards the HASP settings are independent of the dashboard
- widget types: Symbol (MDI symbol, color shows the state, acts as toggle button; with the option
Spin bei Aktivität a ring turns around the symbol while the sensor is on, also for SymbolValue), SymbolValue,
Value, Text, Meter (gauge), MeterLinear (vertical bar with scale, thermometer for °C),
Time (clock and date, rendered by the panel), Level (MDI symbol filled by percentage in the
bar color, e.g.
mdi-water,mdi-propane-tank,mdi-car-battery; range from the scale of the widget) (the fill refers to the drawn height of the symbol; the option Füllung als Fläche fills a rectangle behind the symbol instead, for frame symbols without a filled variant, e.g.mdi-crop-landscapeas a tank) and Choice (the choices of the sensor as button matrix, 2 per row up to 4 choices, otherwise 3; the current value is the highlighted button in the 'on' color, a touch sends the choice to the command topic of the sensor like the dashboard choice widget; without a command topic the selection is reverted) and Balance (horizontal bar with zero in the middle for bipolar values like the battery current or the grid power: negative values fill to the left in the first color (default red), positive to the right in the second (default green); Skala Max is the full scale, the range is symmetric unless a negative Skala Min is given; Richtung tauschen mirrors the bar for sensors reporting discharge positive; Level and Balance exist as dashboard widgets too) and Timer (pseudo sensor Timer in the sensor list: countdown of 1..60 minutes with mm:ss, progress bar and step buttons (Schritte, comma separated minutes or seconds with suffixs, e.g.-30s,30s,5,30, default -1,1,5, up to 6) plus Start-Stop; it runs in the panel firmware and keeps running without WLAN, the board buzzer beeps for a minute when the time is up, any touch on the card stops it; a touch on the time starts/stops, a long press resets to the configured minutes; needs the custom firmware >= 0.7.0.2, see openHASP/README.md) - the pages are sent after saving, with the button An Panel senden and automatically whenever the panel comes online (openHASP forgets objects received via MQTT on reboot); further buttons: Panel wecken (display on, the idle cycle restarts), Panel neu starten and Screenshot (the screenshot page of the panel's web server, the IP is taken from the panel's status report)
- the idle times of the panel (Leerlauf, seconds) are set on the same page: after the short one the
panel dims, after the long one the display goes off (what exactly happens is defined by the scripts
idle_short.cmd/idle_long.cmd/idle_off.cmdon the panel). They are stored as the configuration items haspIdleShort / haspIdleLong and sent with the pages (config/gui,idle1/idle2); 0 switches a stage off, both 0 leaves the panel's own setting untouched - values are pushed on every sensor change; openHASP has no chart object, so charts are not available
With a GPS receiver (see gps.u-blox, sensor type GPS) the page Map of the WEBIF shows the live position.
In addition trips can be recorded as tours and displayed later. Any receiver delivering NMEA 0183 on a
serial port works (USB pucks, modules on a UART), see the receiver notes in gps.u-blox/README.md; the
receiver settings of the GPS configuration are u-blox specific. While a tour is recorded the Map tab
of the main menu and the Touren button of the tool bar are colored (red, orange while paused), the tour
head shows the driving time (duration minus pauses); a pause / night stop entered during an automatic
pause takes over its begin, end and duration.
- Map -> ● Tour aufzeichnen: enter a name and optionally the odometer reading [km] and start the recording (needs control rights), ■ Tour beenden stops it and asks for the odometer at the end. Only one tour can be active at a time.
- while a tour is active the tool bar offers buttons for events: fuel stop (liters, total price,
odometer), toll, pause, night stop (each with optional costs) and a note. Every event is stored
with its time and the current GPS position (table
gpstourevents). The odometer readings at start and end are columns ofgpstours, they can be corrected later together with the name (pencil button in the tour list) - costs and fuel consumption are calculated from the events: total costs (fuel + toll + other), consumption per fuel stop = liters / km since the previous full tank and the average over all fuel stops with a known distance. The chain starts at the last full fuel stop before the tour (of any tour or entered outside of a tour, see below), so the first fill of a tour is not charged to the tour's km only; without any earlier fuel stop the odometer of the tour start is used (marked ab Tour-Start, assumes a full tank at the start). The distance is taken from the odometer readings (Tacho) and in parallel from the GPS distance of the tour at the moment of the fuel stop (GPS, stored with the event, tour-bound by nature), both values are shown. A fuel stop marked nicht voll (partial fill) gets no value of its own, its liters are added to the next full one. They are shown in the tour list, in the info box of the displayed tour and in the control panel while recording
- while a tour is active a point is stored whenever the position moved at least gpsTourMinDistance meters (Setup -> Configuration -> GPS, default 25 m) since the last stored point. Standing still therefore does not produce data.
- without movement for gpsTourPauseAfter minutes (default 5) the tour is paused, on the next movement it continues automatically. The pause time is summed up and shown in the tour list. A GPS receiver drifts by 100-200 m at standstill, therefore 'movement' for the pause means leaving the radius gpsTourPauseRadius (default 200 m) around the position of the last movement; while paused no points are stored. Each pause is kept as event Stillstand with begin, end and duration in the event list of the tour (only its note is editable).
- the active tour survives a restart of the daemon: it is continued automatically, including the distance and the point count.
- the recording is bound to the sensor
GPS:0x0a(Coordinate), it has to be active. While a tour is active its Record flag in Setup -> IO Setup is set by the daemon, when the tour is stopped it is cleared. - u-blox receivers can suppress the drift themselves: Empfänger: Static Hold (Setup -> Configuration -> GPS,
km/h, 0 = off) freezes the position below that speed, Dynamikmodell Automotive selects the vehicle
platform model, Galileo aktivieren switches a u-blox 8 to GPS + Galileo + GLONASS (the u-blox 7 has no
Galileo). The daemon sends them to
gpsmqtt.py(which owns the serial port) at its start, on change and when the script registers, the script writes them into the receiver on every port open (seegps.u-blox/README.md).
The view Sat (satellite button of the tool bar) helps aligning the TV dish: from the live position it
calculates azimuth (true north), elevation and LNB skew to the satellite of TV-Satellit: Orbitposition
(Setup -> Configuration -> GPS, default 19.2 = Astra 19.2 E, west negative) and draws the bearing on the map
(dashed blue arrow of 75 m with the azimuth), so that a landmark in that direction can be picked. The vehicle axis is the course of the
last movement (sensor GPS:0x0b Heading of gpsmqtt.py, kept while standing, has to be active), drawn
in blue with the outline of the motorhome (7 x 2.3 m, the GPS position as its center; orange and pointing
north, gray, as long as the heading is unknown); the box shows the
angle to the satellite relative to it ('44° links') with the vehicle from above,
the switch rückwärts eingeparkt adds 180° when the vehicle was reversed into the pitch. For a crank mast
(TV-Satellit: Kurbel, degrees per turn, default 12 for the Kathrein HDM 140, flat antenna horizontal at 0)
the box shows the crank turns for the elevation, (90° − elevation) / degrees per turn. With the script
sensor dvb-signal.sh the box also shows the signal monitor (lock, level, C/N about once per second) with
start / stop. The sensor is configured as TV-Satellit: Signal-Sensor (e.g. SC:0x30), without it the
view is not offered.
- table
gpstoursholds only the tour itself: name, start, stop, distance [m], number of points and pause time [s] - the points are the samples of
GPS:0x0a(textlat/lng) between start and stop of the tour. With each point the speed (GPS:0x03) and the altitude (GPS:0x05) are stored with the same time stamp (if the sensors are active), e.g. for speed or altitude profiles. Outside of a tour nothing is stored by the tour recording - tour samples have
AGGREGATE = 'T'; they are never aggregated or deleted by the aggregation, so the tours stay complete. Regular samples ('S', sensors with the Record flag) are aggregated as before - deleting a tour in the WEBIF also deletes its samples (coordinate, speed, altitude) from
samplesand its events fromgpstourevents
- Map -> Live: the map follows the live position (button Live-Tracking on the map)
- on stop the daemon proposes the arrival at the final position as end of the tour (the time the position came within Tour: Toleranz Ankunft meters of the final position and stayed there, default 200 m) - for a forgotten stop or a long wait at the reception; jetzt is offered as well
- Map -> Touren: the recorded tours are listed by name in the panel on the left, the selected one shows its details on the right (start, end, duration, pause, distance, odometer, costs, consumption) and the list of its events (time, kind, liters, price, odometer, consumption, note) where events can be changed or deleted (control rights); Karte draws the tour on the map (green marker = start, red marker = end) with the events as icons, their popup shows the details and offers the same actions; the pencil button changes name, odometer readings and a free comment for the whole tour (shown above the details), the trash button deletes the tour (control rights). The active tour can be displayed too but not deleted. Entering an event never changes the view.
- Zentrieren (button on the map) brings the whole route into view
- Map -> Tankstopps (gas station icon) lists all fuel stops: the ones entered during tours (with a link to the tour) and the ones entered here outside of a tour (Tankstopp eintragen, control rights) with date / time, odometer, liters, total price, voll getankt and a note. The position is the current GPS position if available
- the consumption is calculated across the tours from full tank to full tank by the odometer readings: liters / km since the previous full fuel stop, partial fills are added to the next full one. The head shows the average over all counted fills, the total liters and costs and the average price per liter. A fuel stop without odometer or the first one gets no value
- the fuel stops are the events of type
fuelingpstourevents, standalone ones haveTOURID = 0; editing / deleting works from this list as well as from the tour
The tab Garmin shows the activities of a Garmin Connect account (e.g. recorded with a Fenix
watch), grouped by activity type. It is offered when the Garmin login exists (/etc/homectld/garmin).
- installation and the one time login: see
garmin/README.md(garmin.py login, works with and without two factor authentication, the tokens are valid for about a year) - nothing is polled - the activities are fetched only with the button Abgleich mit Garmin, which asks for the period (since the last activity, since a date or all). Data volume about 3-4 kB per activity, tracks are not downloaded
- the activities are stored in the table
activities, not as sensors and not insamples - groups per type are collapsible and show count, total duration, total distance and the maximum speed; water sports (windsurfing, kiteboarding, sailing, ...) show the speeds in knots, running and walking as pace
- Details shows the values of the activity. The sync already stores most of them from the list entry (speeds, heart rate, calories, elevation, training effect, min/max temperature, ...) as a preliminary version; Details nachladen fetches the rest from Garmin (min. heart rate, average temperature), the dot in the list marks the fully loaded ones
- a click on a row opens the details, the pencil button a dialog to change the type (e.g. activities recorded with a wrong profile), the name and the location at Garmin, the trash button deletes the activity at Garmin (after confirmation); both need control rights
- bulk edit: check boxes in the rows (the one in the group header selects the whole group, a click with Shift or Ctrl selects all rows from the last clicked one to this one), Auswahl bearbeiten changes type, name and/or location of all selected activities, empty fields stay as they are; Garmin has no bulk update, garmin.py sends one request per activity with all changed fields (about 1 s each)
- filters: type, period, name and location (name / location are regular expressions, the location field suggests the known locations); a click on a column header sorts the lists (again: reverse)
- Track in the details loads the GPS track, simplified by Garmin to 1000 points or at least one
point per 15 seconds, and shows it on a map colored by speed, hovering a segment shows speed, time
and altitude; details and tracks are cached in the tables
activities/activitytracks, Neu laden fetches them again - distance check: the watch sums up (almost) no distance in some water sport sessions although the GPS
track is complete (e.g. 6 m for 35 minutes of windsurfing). When a track is loaded its length (sum of
the segments) is stored in
activities.trackdistance; if Garmin's distance deviates by more than the configured percentage (Distanz aus dem Track ab Abweichung, default 50 %, 0 = off) the list, the group sums and the details show the track's distance (in italics, the details show both values). The sync of the list does not load tracks, so the check only applies to activities with a loaded track - Karte: world map with the start positions of the activities as points colored by type (the legend filters by type), a click on a point opens the track; the period, name and location filters apply
- the maps have a Zentrieren button which brings the whole route / all points into view
How to enable i2c on a Rasperyy Pi is well documented in the www
for Odroid I use Armbian (noble), you find a script to check the i2c bus in the contrib folder. It supports the n2+ and the m1! To check:
./contrib/check-i2c-header.sh
...
...
=== Ergebnis: I2C auf Pins 3/5 verfügbar ===
Scan: i2cdetect -y 2
-> all fine. If Not try to fix the device tree by calling:
./contrib/armbian-enable-header-i2c.sh
Board: Hardkernel ODROID-N2Plus
→ N2+ erkannt: DTB-Patch
DTB: /boot/dtb/amlogic/meson-g12b-odroid-n2-plus.dtb (→ /boot/dtb-6.18.8-current-meson64/amlogic/meson-g12b-odroid-n2-plus.dtb)
i2c2_sda_x phandle: 0x82 (130)
i2c2_sck_x phandle: 0x83 (131)
Aktuell status: status=okay
Aktuell pinctrl-0: pinctrl-0 = <0x82 0x83>;
Bereits korrekt — kein Patch nötig.
If you use the i2c2mqtt service shipped with the homectld you hav to use tbe i2c bus number provided by the check scipt, in case of the output above 2. Therefor use the device /dev/i2c-2.
To scan the bus:
root@odroidn2 (gpiod-dev u=) ~/source/homectld> i2cmqtt -d /dev/i2c-2 -S
start scan of i2c device '/dev/i2c-2' ..
0x38
.. done
And to read this sensor (In my case it's a DHT20) call:
root@odroidn2 (gpiod-dev u=) ~/source/homectld> i2cmqtt -d /dev/i2c-2 --dht 0x38 -s
-----------------------
DHT38
Temperature 23.95 °C
Humidity 51 %
-----------------------
A INA219 current/power monitor (default i2c address 0x40) is read with:
root@odroidn2 (gpiod-dev u=) ~/source/homectld> i2cmqtt -d /dev/i2c-2 --ina 0x40 -s
-----------------------
INA40 (shunt 0.1000 Ohm)
Bus voltage 12.416 V
Shunt voltage 21.340 mV
Current 0.213 A
Power 2.650 W
-----------------------
The shunt resistor defaults to 0.1 Ohm, which is the value most breakout boards are equipped with.
If your board uses another one append it to the address, e.g. --ina 0x40:0.01 for a 10 mOhm shunt.
Voltage, current and power are published as three values with the addresses 0, 1 and 2.
calling:
i2cmqtt --help
show the options and supported sensor types. You can start it as a service and provide the data by MQTT to the homerctld.
systemctl start homectld
systemctl enable homectld
systemctl status homectld
it should now 'enabled' and in state 'running'!
grep -i "error" /var/log/homectld.log
The default username and password for login to the web interface is:
User: homectl
Password: homectl
The default Port is 61109. You reacg the WEB interface with this url (replace your-ip):
kiosk - start in kiosk mode:
// 0 - with menu, normal dash symbols, use normal-widget-height-factor // 1 - without menu, big dash symbols, use kiosk-widget-height-factor // 2 - with menu, big dash symbols, use kiosk-widget-height-factor // 3 - with menu, big dash symbols, use normal-widget-height-factor
The big dash symbols can also be enabled permanently by the config option 'Große Dashboard Symbole' (Setup -> Konfiguration -> WEB Interface), the URL parameter kiosk has priority.
heightFactor group backTime page - specify the start page (dashboard, setup, chart, ...) dash - specify the start page of the dashboard (dashbord ID)
For example to start in kiosk mode 2
http://your-ip:61109/index.html&kiosk=2
The WEB interface can be added to the iOS home screen (Safari -> share -> 'Add to Home Screen') and runs as a standalone app then. The status bar is transparent, the menu row is fixed at the top (since iOS 27 the status bar blurs scrolling content below it, a fixed element stays sharp). Note: iOS caches the page settings per icon, after an update of homectld delete and re-add the icon if the layout looks odd.
- Log in to the web interface
- Go to Setup -> IO Setup
- Enable the desired sensors (for example, script sensors)
Script sensors are the scripts in /etc/homectld/scripts.d (templates example_sensor.sh and
example_switch.sh), one sensor per script. dvb-signal.sh is a switch for aligning a satellite
dish: a click starts a monitor which reads dvb-fe-tool --femon (package dvb-tools) and shows
lock, signal level and C/N about once per second as the text of the widget (type SymbolText), a
second click or 30 minutes stop it. The adapter number is the argument of the sensor (IO setup,
default 0); a lock and the C/N need a tuned frontend, e.g. a running stream of minisatip.
Some sensor categories are only available once the corresponding interface service has been activated and is running. You can implement this interface service yourself using C/C++, Python, a shell script, and many other languages. The interface between these services and homectld is MQTT, it's not yet fully documented here; for now, you can only refer to the examples and the code.
Services are already provided for some common components/devices:
- Theford refrigerators
- Mopeka gas level sensor
- One-Wire sensors
- Votronic MPP charge controller
- Victron (at least the interface to the inverter is already working with it)
- RTL433 wireless sensors
- BMS (the widely used version, which is also used by Liontron)
- reboot the device to check if homectld is starting automatically during startup
homectld embeds a Lua 5.3 scripting engine that lets you compute values and control digital outputs directly inside the daemon — no external process needed.
| Sensor type | Purpose |
|---|---|
| CV (Calculated Value) | Compute a numeric, boolean, or text result from one or more other sensors |
| DO (Digital Output, Auto mode) | Decide automatically whether an output should be on or off |
Scripts are entered in the sensor's settings in the web interface (Setup → IO Setup → edit the sensor).
A script is executed when at least one sensor declared via watch() has changed its value since the
last run of the script, or when the interval requested via every(seconds) has elapsed.
The very first run (at startup and after the script was saved) is always executed so that the watch()
and every() calls can be collected. From then on the daemon only re-runs the script when one of them is
due. A script declaring neither watch() nor every() is run every 60 seconds. A script with watch()
only runs on changes of the watched sensors, so a script that also depends on the passing of time (hold
times, timeouts, runtime counters) has to add an every().
All known sensors are available as a nested table:
sensors["TYPE"][address]TYPE is the sensor type string (e.g. "AI", "DI", "DO", "CV", "OW", "WEA", …).
address is the numeric address of the sensor — you can write it as a decimal integer or as a hex literal:
sensors["AI"][1] -- decimal address 1
sensors["DO"][0x02] -- hex address 2 (both forms are equivalent)| Field | Type | Description |
|---|---|---|
value |
number | Numeric measurement value |
state |
boolean | Boolean state (for status-kind sensors and digital outputs) |
text |
string | Text value |
kind |
string | "value", "status", "text", or "trigger" |
last |
integer | Unix timestamp of the last received update |
changedAt |
integer | Unix timestamp of the last actual value change |
name |
string | Internal name |
title |
string | Display title |
unit |
string | Unit string (e.g. "°C", "V", "%") |
valid |
boolean | true if the sensor has delivered at least one valid reading |
All fields are read-only — the script cannot modify sensor data directly.
Declares a sensor the script depends on. The script is re-run as soon as that sensor has changed since the last run (within 0.5 s, GPIO inputs immediately when read).
watch("GPIO", 0x0c)Requests a run in the given interval, in addition to the watch() dependencies and the forced run
once per minute. Useful for scripts that accumulate time or poll something; the resolution is one second,
the actual call happens within 0.5 s after the interval has elapsed.
every(10) -- run at least every 10 secondsWrites a message to the homectld log. level is one of the predefined constants:
| Constant | When visible |
|---|---|
eloAlways |
always |
eloInfo |
info verbosity |
eloLua |
Lua verbosity |
eloDebug |
debug verbosity |
Lua handles the formatting itself via string.format:
tell(eloLua, "temperature is " .. sensors["AI"][1].value)
tell(eloAlways, string.format("value out of range: %.2f", sensors["AI"][1].value))true when the script was triggered by an explicit signal/event, false during a regular cyclic evaluation.
The script must return a value. The type of the returned value determines what is written back to the CV sensor:
| Lua return type | Effect on the sensor |
|---|---|
number (integer) |
value field is updated as integer |
number (float) |
value field is updated as float |
boolean |
state field is updated |
string |
text field is updated |
no return / nil |
nothing is written (warning logged) |
watch("AI", 1)
watch("AI", 2)
local a = sensors["AI"][1]
local b = sensors["AI"][2]
if not a.valid or not b.valid then
return 0.0
end
return (a.value + b.value) / 2.0watch("AI", 0x01)
local temp = sensors["AI"][0x01]
if not temp.valid then
return false
end
return temp.value > 25.0 -- true = ON, false = OFFwatch("DO", 1)
watch("AI", 3)
local pump = sensors["DO"][1]
local flow = sensors["AI"][3]
if pump.state then
return string.format("running, flow %.1f l/min", flow.value)
else
return "stopped"
endwatch("AI", 1)
if signal then
return sensors["AI"][1].value * 2.0
end
return sensors["AI"][1].value- Always call
watch()for every sensor the script reads — only watched sensors trigger re-evaluation. - Use
sensor.validbefore accessingsensor.valueorsensor.stateto avoid stale data on startup. - Both decimal (
1) and hex (0x01) address literals work inwatch()and insensors[...]access. - The standard Lua libraries (
math,string,table,os, …) are available.
The application dynamically renders weather widgets based on the data received from the backend. Depending on your configuration and hardware setup, two distinct sensor outputs are generated:
-
Windy App by Spot ID This widget displays the wind and weather forecast for your fixed, pre-configured favorite windsurfing location. It uses the
Windy Spot IDdefined in your settings and will load this specific spot consistently, making it ideal for checking your home spot. -
Windy App by Spot GPS This widget automatically adapts to your current physical position. If a GPS receiver is connected and active (e.g., while traveling in your camper), the system uses your live coordinates to automatically find and display the nearest windsurfing spot registered in the Windy.app database.
The weather widgets require a free Application ID from Windy.app. You do not need to register a developer account; the ID is automatically generated directly through their website.
- Go to any weather spot page on Windy.app (e.g., the Gialova Navarino Spot).
- Scroll down the page until you find the section titled "Live wind map. Install on your site." or "Weather widget".
- Look at the generated HTML code snippet. It will look like this:
<div data-windywidget="forecast" data-thememode="white" data-spotid="444217" data-appid="e6865e0c4a1713e956bd11fc0b5fec63"></div>
- Open the Windy.app Widget Generator.
- Choose your preferred widget layout.
- Click the button to generate the HTML code.
- Extract the hex string found inside the
data-appidfield from the generated code block.
Copy this number and paste it into your configuration (Setup -> WEB Interface -> Windy App Spot ID).
⚠️ Note on thewidgets_prefix: If this application automatically prependswidgets_to the key in the frontend, you only need to provide the raw 32-character key in your configuration file. Do not include thewidgets_prefix yourself.
If you want the widget to always load a specific favorite windsurfing location by default, you can provide its unique Spot ID:
- Open your browser and go to Windy.app.
- Use the search bar to find your favorite windsurfing spot (e.g., search for "Gialova Navarino").
- Look at the URL in your browser's address bar. It will look like this:
https://windy.app - The number right after
/spot/is your unique Spot ID. In this example, the ID is 444217.
Copy this number and paste it into your configuration (Setup -> WEB Interface -> Windy App Spot ID). Every spot ID stored there is remembered and offered as suggestion of the input field with the spot's name (the setup page asks Windy for it), so a previously used spot can be picked by name; the trash button forgets the entered value. Windy offers no lookup of a spot by name, only the ID is accepted.
Some specials for a 'Stand Alone Server' like I use in my camper. Since the mini PC (in my case, an Odroid N2+) is being used as a server in this scenario, the network setup is different: eth0 has a static IP address and cannot access the internet. Internet access is provided by a Wi-Fi dongle configured as wlan0. A Wi-Fi access point for the RV’s “internal” network is connected to eth0.
The server also handles IP masquerading to provide all internal clients with Internet access. Additionally, dnsmasq runs as a DNS server/relay and DHCP service. Crony is used instead of systemd-timesyncd because the latter proved to be too “fussy” in this setup.
To enable the install some of the provided service configurations
enable it by setting 'WOMO = 1' in Make.user in the projects (source) root directory of the homectld.
make config edits Make.user interactively (switches and personalization, needs apt install dialog),
the possible settings and their defaults are in Make.user.template.
apt -y install iptables openvpn
to get a fixed IP for your eth0 device you find a teplate in contrib/womo/netplan/ just copy it to /etc/netplan:
cp contrib/womo/netplan/01-netcfg.yaml /etc/netplan/
and adjust the settings, such as the IP address, to your preferences. The route set there can be deleted, but it generally doesn't cause any problems. In my setup, it is used to route SatIp broadcat signals from the Minisatip service which I'm running on the Odroid.
apt -y install dnsmasq dnsmasq-utils
if you call 'make install' of the homectld AFTER the install of dnsmasq the setup (/etc/dnsmasq*) will prepared automatically,
just adjust your inrternal internal subnet by setting SUBNET = SUBNET = 192.168.220 in Make.user (to your subnet)
Falls es Aufgrund der Art der Netzwerk/Internet Anbindung mit dem systemd-timesyncd zu Probmelmen kommt auf chroony umstellen
apt -y install chrony
systemctl disable --now systemd-timesyncd
systemctl enable --now chrony
chronyc tracking
timedatectl
to act as a router some settings IP table rules are necessary, this are prepared in the fwpn script. which is also set up as a systems service (fwpn.service). All you have to do is update your subnet in /etc/default/fwpn. The other options in fwpn can also be overridden via /etc/default/fwpn. Note that the MSQ_DEV in fwpn can be switched via the msqdevice.sh script service in the web frontend if the internet is accessed via usb0 rather than wlan0, for example
apt install modemmanager usb-modeswitch libqmi-utils network-manager
Für den Internet Zugangmittels einem Alfa Tube-U4G LTE USB Modem wird das Script lte-autoconnect.sh nebst zugehörigen Service lte-modem.service
installiert. Damit kann die Verbindung mittels:
systemctl start lte-modem.service
systemctl stop lte-modem.service
gestartet bzw. wieder beendet werden. Das ist auch ins Dashboard der GUI integriert. Sofern die Verbindung nach jedem boot automatisch aktiviert werden soll kann man den Service stattdessen enabeln:
systemctl enable lte-modem.service
mmcli -m $(mmcli -L | grep -oP 'Modem/\K[0-9]+') --signal-get
Alle 5 Minuten mittels crontab Eintrag checken
(crontab -l 2>/dev/null | grep -Fv "/usr/local/bin/lte-autoconnect.sh"; echo "*/5 * * * * /usr/local/bin/lte-autoconnect.sh > /dev/null 2>&1") | crontab -
Wenn der Service 'lte-modem.service' absichtlich 'aus' ist führt das NICHT zu Neu-Verbindung!
Das Tube-U4G kann manchmal viel Strom ziehen (besonders bei schlechtem Empfang beim Senden). Da am Odroid bereits viele USB-Geräte hängen achte darauf, dass der Tube-U4G an einem der USB 3.0 Ports (blau) hängt. Falls es Verbindungsabbrüche gibt, könnte ein aktiver USB-Hub mit eigener Stromversorgung nötig sein, aber der N2+ liefert an den blauen Ports normalerweise genug Saft.
Backup the data of the homectl database including all recorded values:
homectld-backup.sh
This will create the at least this dumps:
config-dump.sql.gz
errors-dump.sql.gz
jobs-dump.sql.gz
menu-dump.sql.gz
samples-dump.sql.gz
schemaconf-dump.sql.gz
sensoralert-dump.sql.gz
smartconfig-dump.sql.gz
valuefacts-dump.sql.gz
timeranges-dump.sql.gz
hmsysvars-dump.sql.gz
scripts-dump.sql.gz
...
...
To import the backup:
gunzip NAME-dump.sql.gz
mysql -u homectl -phomectl -homectl < NAME-dump.sql.gz
replace NAME with the name of the dump
If you like to connect a Battery-Management-Systems like them used commonly from LionTron a you can use
the bmsmqtt service shipped with the homectld. Jus configure it in /etc/default/bmsmqtt, enable and start it:
systemctl enable bmsmqtt.service
systemctl start bmsmqtt.service
The One Wire Sensors are managed by the w1mqtt service shipped with homectld, this service is enables by default and can be disabled if not needed.
Disable w1mqtt
systemctl disable w1mqtt.service
The w1mqtt service is configured in /etc/default/w1mqtt
A RTL Radio/USB adapter like the https://amzn.eu/d/bKBwTOO is needed. As Sensor you van use any compatible, mostly all 433Mht radio sensors.
For the interface to the homectld you can use rtl-433 which is includes in most linix distributions, whis can configured to write the data directly
to MQTT where it's read and paresed by the homectld.
To install the rtl-433 package:
apt -y install rtl-433
To start the rtl-433 service a Systemd unit file is shipped with the homectld, install, enable and start it by:
cd linux-homectld
cp contrib/rtl433.service /etc/systemd/system/
systemctl enable rtl433.service
systemctl start rtl433.service
To change the MQTT borker connection (host, port, ...) adjust it in /etc/systemd/system/rtl433.service and restart the service.
The CI bus adapter https://www.fischl.de/usblini/ is a lightweight solution. Details descibed here ./thetford/README.md
If you cannot figure out why you get Access denied, remove all entries from the user table that
have Host values with wildcards contained (entries that match % or _ characters).
A very common error is to insert a new entry with Host='%' and User='some_user', thinking that this allows you to specify localhost to connect from the same machine.
The reason that this does not work is that the default privileges include an entry with Host='localhost' and User=''. Because that entry has a Host value localhost that is more specific than %, it is used in preference to the new entry when connecting from localhost! The correct procedure is to insert a second entry with Host='localhost' and User='some_user', or to delete the entry with Host='localhost' and User=''.
After deleting the entry, remember to issue a FLUSH PRIVILEGES statement to reload the grant tables.
To analyze this you can show all users:
use mysql
SELECT host, user FROM user;
https://www.raspberrypi.org/documentation/usage/gpio/ https://projects.drogon.net/raspberry-pi/wiringpi/pins/ http://wiringpi.com/reference/setup/
Overview of IO pins
apt -y install python3-gpiozero
pinout
wget https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.min.css copy to htdocs/css/
Rolladen an Hommatic CCU mit homectld ID 2 'HMB:2' komplette schließen mosquitto_pub --quiet -L mqtt://192.168.200.101:1883/homectld2mqtt/nodered -m '{ "id":"HMB:2","bri":0 }' auf 50% mosquitto_pub --quiet -L mqtt://192.168.200.101:1883/homectld2mqtt/nodered -m '{ "id":"HMB:2","bri":50 }'
-
Relay Board with at least 8 relays to switch 230V and a max current which fit the needs of your components (lights, pumps, ...) for example: https://www.amazon.de/gp/product/B014L10Q52/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1
-
At least 3 One Wire sensors for example: https://www.amazon.de/gp/product/B00CHEZ250/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1
-
Power sockets to connect lights and pumps for example: https://www.amazon.de/gp/product/B07CM9DLHY/ref=ppx_yo_dt_b_search_asin_image?ie=UTF8&psc=1
-
230V Magnet Valve (for the shower) for example: https://www.amazon.de/gp/product/B072JBBHJS/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1
-
Button (for shower) for example: https://www.amazon.de/gp/product/B002LE8EJC/ref=ppx_yo_dt_b_search_asin_image?ie=UTF8&psc=1
-
PH probe for example: https://www.amazon.de/gp/product/B081QK9TX2/ref=ppx_yo_dt_b_asin_title_o06_s00?ie=UTF8&psc=1
-
Arduino Pro Mini 328 (for PH probe and PH minus fluid control) for example: https://www.amazon.de/gp/product/B015MGHLNA/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&psc=1 USB Interface for programming: https://www.amazon.de/gp/product/B07KVT6HNL/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1
-
peristaltic pump to inject PH Minus for example: https://www.amazon.de/gp/product/B07YWY29XL/ref=ppx_yo_dt_b_asin_title_o07_s00?ie=UTF8&psc=1 better but more expensive may be: https://www.amazon.de/dp/B06ZZDLTJ7/?coliid=I37J0L29HIAMDR&colid=344R3XZTD8676&psc=1&ref_=lv_ov_lig_dp_it
-
... to be completed !





