1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.ebusd;
9in
10{
11 meta.maintainers = with lib.maintainers; [ nathan-gs ];
12
13 options.services.ebusd = {
14 enable = lib.mkEnableOption "ebusd, a daemon for communication with eBUS heating systems";
15
16 package = lib.mkPackageOption pkgs "ebusd" { };
17
18 device = lib.mkOption {
19 type = lib.types.str;
20 default = "";
21 example = "IP:PORT";
22 description = ''
23 Use DEV as eBUS device [/dev/ttyUSB0].
24 This can be either:
25 enh:DEVICE or enh:IP:PORT for enhanced device (only adapter v3 and newer),
26 ens:DEVICE for enhanced high speed serial device (only adapter v3 and newer with firmware since 20220731),
27 DEVICE for serial device (normal speed, for all other serial adapters like adapter v2 as well as adapter v3 in non-enhanced mode), or
28 [udp:]IP:PORT for network device.
29
30 Source: <https://github.com/john30/ebusd/wiki/2.-Run#device-options>
31 '';
32 };
33
34 port = lib.mkOption {
35 default = 8888;
36 type = lib.types.port;
37 description = ''
38 The port on which to listen on
39 '';
40 };
41
42 readonly = lib.mkOption {
43 type = lib.types.bool;
44 default = false;
45 description = ''
46 Only read from device, never write to it
47 '';
48 };
49
50 configpath = lib.mkOption {
51 type = lib.types.str;
52 default = "https://cfg.ebusd.eu/";
53 description = ''
54 Directory to read CSV config files from. This can be a local folder or a URL.
55 '';
56 };
57
58 scanconfig = lib.mkOption {
59 type = lib.types.str;
60 default = "full";
61 description = ''
62 Pick CSV config files matching initial scan ("none" or empty for no initial scan message, "full" for full scan, or a single hex address to scan, default is to send a broadcast ident message).
63 If combined with --checkconfig, you can add scan message data as arguments for checking a particular scan configuration, e.g. "FF08070400/0AB5454850303003277201". For further details on this option,
64 see [Automatic configuration](https://github.com/john30/ebusd/wiki/4.7.-Automatic-configuration).
65 '';
66 };
67
68 logs =
69 let
70 # "all" must come first so it can be overridden by more specific areas
71 areas = [
72 "all"
73 "main"
74 "network"
75 "bus"
76 "device"
77 "update"
78 "other"
79 ];
80 levels = [
81 "none"
82 "error"
83 "notice"
84 "info"
85 "debug"
86 ];
87 in
88 lib.listToAttrs (
89 map (
90 area:
91 lib.nameValuePair area (
92 lib.mkOption {
93 type = lib.types.enum levels;
94 default = "notice";
95 example = "debug";
96 description = ''
97 Only write log for matching `AREA`s (${lib.concatStringsSep "|" areas}) below or equal to `LEVEL` (${lib.concatStringsSep "|" levels})
98 '';
99 }
100 )
101 ) areas
102 );
103
104 mqtt = {
105 enable = lib.mkEnableOption "support for MQTT";
106
107 host = lib.mkOption {
108 type = lib.types.str;
109 default = "localhost";
110 description = ''
111 Connect to MQTT broker on HOST.
112 '';
113 };
114
115 port = lib.mkOption {
116 default = 1883;
117 type = lib.types.port;
118 description = ''
119 The port on which to connect to MQTT
120 '';
121 };
122
123 home-assistant = lib.mkOption {
124 type = lib.types.bool;
125 default = false;
126 description = ''
127 Adds the Home Assistant topics to MQTT, read more at [MQTT Integration](https://github.com/john30/ebusd/wiki/MQTT-integration)
128 '';
129 };
130
131 retain = lib.mkEnableOption "set the retain flag on all topics instead of only selected global ones";
132
133 user = lib.mkOption {
134 type = lib.types.str;
135 description = ''
136 The MQTT user to use
137 '';
138 };
139
140 password = lib.mkOption {
141 type = lib.types.str;
142 description = ''
143 The MQTT password.
144 '';
145 };
146 };
147
148 extraArguments = lib.mkOption {
149 type = lib.types.listOf lib.types.str;
150 default = [ ];
151 description = ''
152 Extra arguments to the ebus daemon
153 '';
154 };
155 };
156
157 config =
158 let
159 usesDev = lib.any (prefix: lib.hasPrefix prefix cfg.device) [
160 "/"
161 "ens:/"
162 "enh:/"
163 ];
164 in
165 lib.mkIf cfg.enable {
166 systemd.services.ebusd = {
167 description = "EBUSd Service";
168 wantedBy = [ "multi-user.target" ];
169 after = [ "network.target" ];
170 serviceConfig = {
171 ExecStart =
172 let
173 args = lib.cli.toGNUCommandLineShell { optionValueSeparator = "="; } (
174 lib.foldr (a: b: a // b) { } [
175 {
176 inherit (cfg)
177 device
178 port
179 configpath
180 scanconfig
181 readonly
182 ;
183 foreground = true;
184 updatecheck = "off";
185 log = lib.mapAttrsToList (name: value: "${name}:${value}") cfg.logs;
186 mqttretain = cfg.mqtt.retain;
187 }
188 (lib.optionalAttrs cfg.mqtt.enable {
189 mqtthost = cfg.mqtt.host;
190 mqttport = cfg.mqtt.port;
191 mqttuser = cfg.mqtt.user;
192 mqttpass = cfg.mqtt.password;
193 })
194 (lib.optionalAttrs cfg.mqtt.home-assistant {
195 mqttint = "${cfg.package}/etc/ebusd/mqtt-hassio.cfg";
196 mqttjson = true;
197 })
198 ]
199 );
200 in
201 "${cfg.package}/bin/ebusd ${args} ${lib.escapeShellArgs cfg.extraArguments}";
202
203 DynamicUser = true;
204 Restart = "on-failure";
205
206 # Hardening
207 CapabilityBoundingSet = "";
208 DeviceAllow = lib.optionals usesDev [
209 (lib.removePrefix "ens:" (lib.removePrefix "enh:" cfg.device))
210 ];
211 DevicePolicy = "closed";
212 LockPersonality = true;
213 MemoryDenyWriteExecute = false;
214 NoNewPrivileges = true;
215 PrivateDevices = !usesDev;
216 PrivateUsers = true;
217 PrivateTmp = true;
218 ProtectClock = true;
219 ProtectControlGroups = true;
220 ProtectHome = true;
221 ProtectHostname = true;
222 ProtectKernelLogs = true;
223 ProtectKernelModules = true;
224 ProtectKernelTunables = true;
225 ProtectProc = "invisible";
226 ProcSubset = "pid";
227 ProtectSystem = "strict";
228 RemoveIPC = true;
229 RestrictAddressFamilies = [
230 "AF_INET"
231 "AF_INET6"
232 ];
233 RestrictNamespaces = true;
234 RestrictRealtime = true;
235 RestrictSUIDSGID = true;
236 SupplementaryGroups = [ "dialout" ];
237 SystemCallArchitectures = "native";
238 SystemCallFilter = [
239 "@system-service @pkey"
240 "~@privileged @resources"
241 ];
242 UMask = "0077";
243 };
244 };
245 };
246}