diff --git a/debian/control b/debian/control
index f129778b..ddaa1241 100644
--- a/debian/control
+++ b/debian/control
@@ -185,6 +185,14 @@ Description: nymea integration plugin for Schrack wallboxes
This package contains the nymea integration plugin for Schrack wallboxes.
+Package: nymea-plugin-senseair
+Architecture: any
+Depends: ${shlibs:Depends},
+ ${misc:Depends},
+Description: nymea integration plugin for Senseair sensors
+ This package contains the nymea integration plugin for Senseair sensors.
+
+
Package: nymea-plugin-sma
Architecture: any
Depends: ${shlibs:Depends},
diff --git a/debian/nymea-plugin-senseair.install.in b/debian/nymea-plugin-senseair.install.in
new file mode 100644
index 00000000..8dbaf24a
--- /dev/null
+++ b/debian/nymea-plugin-senseair.install.in
@@ -0,0 +1,2 @@
+usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginsenseair.so
+senseair/translations/*qm usr/share/nymea/translations/
diff --git a/nymea-plugins-modbus.pro b/nymea-plugins-modbus.pro
index 5387f044..d0192021 100644
--- a/nymea-plugins-modbus.pro
+++ b/nymea-plugins-modbus.pro
@@ -18,6 +18,7 @@ PLUGIN_DIRS = \
mypv \
phoenixconnect \
schrack \
+ senseair \
sma \
stiebeleltron \
sunspec \
diff --git a/senseair/README.md b/senseair/README.md
new file mode 100644
index 00000000..a441cee7
--- /dev/null
+++ b/senseair/README.md
@@ -0,0 +1,10 @@
+# Senseair
+
+Connects Senseair sensors to nymea. Currently supported sensors are:
+
+* S8 CO2 sensor
+
+# Requirements
+
+* A working RS485 (Modbus RTU) connection to the sensor.
+* A Modbus RTU master configured in nymea with baudrate 9600, 8 data bits, 1 stop bits and no parity.
diff --git a/senseair/ba-senseair-s8-modbus.pdf b/senseair/ba-senseair-s8-modbus.pdf
new file mode 100644
index 00000000..3ea41d64
Binary files /dev/null and b/senseair/ba-senseair-s8-modbus.pdf differ
diff --git a/senseair/integrationpluginsenseair.cpp b/senseair/integrationpluginsenseair.cpp
new file mode 100644
index 00000000..b3e44909
--- /dev/null
+++ b/senseair/integrationpluginsenseair.cpp
@@ -0,0 +1,147 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#include "integrationpluginsenseair.h"
+#include "plugininfo.h"
+#include "hardwaremanager.h"
+#include "hardware/modbus/modbusrtuhardwareresource.h"
+
+IntegrationPluginSenseAir::IntegrationPluginSenseAir()
+{
+
+}
+
+void IntegrationPluginSenseAir::discoverThings(ThingDiscoveryInfo *info)
+{
+ foreach (ModbusRtuMaster *modbusMaster, hardwareManager()->modbusRtuResource()->modbusRtuMasters()) {
+ qCDebug(dcSenseAir()) << "Found RTU master resource" << modbusMaster;
+ if (modbusMaster->connected() && modbusMaster->baudrate() == 9600 && modbusMaster->dataBits() == 8 && modbusMaster->stopBits() == 1 && modbusMaster->parity() == QSerialPort::NoParity) {
+ ParamList parameters;
+ ThingDescriptor thingDescriptor(s8ThingClassId, "Modbus RTU master", modbusMaster->serialPort());
+ parameters.append(Param(s8ThingRtuMasterParamTypeId, modbusMaster->modbusUuid()));
+ thingDescriptor.setParams(parameters);
+ info->addThingDescriptor(thingDescriptor);
+ } else {
+ qCWarning(dcSenseAir()) << "Found configured resource" << modbusMaster << "but it is not connected. Skipping.";
+ }
+ }
+
+ QString displayMessage;
+ if (info->thingDescriptors().count() == 0) {
+ displayMessage = QT_TR_NOOP("Please set up a Modbus RTU master with baudrate 9600, 8 data bits, 1 stop bits and no parity first.");
+ }
+
+ info->finish(Thing::ThingErrorNoError, displayMessage);
+}
+
+void IntegrationPluginSenseAir::setupThing(ThingSetupInfo *info)
+{
+ Thing *thing = info->thing();
+ qCDebug(dcSenseAir()) << "Setup" << thing << thing->params();
+
+
+ if (m_s8Connections.contains(thing)) {
+ qCDebug(dcSenseAir()) << "Reconfiguring existing thing" << thing->name();
+ m_s8Connections.take(thing)->deleteLater();
+ }
+
+ ModbusRtuMaster *master = hardwareManager()->modbusRtuResource()->getModbusRtuMaster(thing->paramValue(s8ThingRtuMasterParamTypeId).toUuid());
+ if (!master) {
+ info->finish(Thing::ThingErrorHardwareNotAvailable, QT_TR_NOOP("The Modbus RTU master is not available."));
+ return;
+ }
+
+ SenseAirS8ModbusRtuConnection *s8Connection = new SenseAirS8ModbusRtuConnection(master, 0xfe, this);
+ connect(info, &ThingSetupInfo::aborted, s8Connection, &SenseAirS8ModbusRtuConnection::deleteLater);
+
+ connect(s8Connection, &SenseAirS8ModbusRtuConnection::reachableChanged, thing, [s8Connection, thing](bool reachable){
+ qCDebug(dcSenseAir()) << "Reachable state changed" << reachable;
+ if (reachable) {
+ s8Connection->initialize();
+ } else {
+ thing->setStateValue(s8ConnectedStateTypeId, false);
+ }
+ });
+ connect(s8Connection, &SenseAirS8ModbusRtuConnection::initializationFinished, info, [=](bool success){
+ qCDebug(dcSenseAir()) << "Initialisation finished" << success;
+ if (success) {
+ qCDebug(dcSenseAir()) << "Meter status:" << s8Connection->meterStatus();
+ m_s8Connections.insert(thing, s8Connection);
+ info->finish(Thing::ThingErrorNoError);
+ } else {
+ delete s8Connection;
+ info->finish(Thing::ThingErrorHardwareNotAvailable);
+ }
+ });
+ connect(s8Connection, &SenseAirS8ModbusRtuConnection::initializationFinished, thing, [=](bool success){
+ if (success) {
+ thing->setStateValue(s8ConnectedStateTypeId, true);
+ }
+ });
+
+ connect(s8Connection, &SenseAirS8ModbusRtuConnection::spaceCo2Changed, thing, [=](quint16 spaceCo2){
+ qCDebug(dcSenseAir()) << "CO2 changed:" << spaceCo2;
+ thing->setStateValue(s8Co2StateTypeId, spaceCo2);
+ });
+
+ s8Connection->update();
+}
+
+void IntegrationPluginSenseAir::postSetupThing(Thing *thing)
+{
+ Q_UNUSED(thing)
+ if (!m_pluginTimer) {
+ qCDebug(dcSenseAir()) << "Starting plugin timer...";
+ m_pluginTimer = hardwareManager()->pluginTimerManager()->registerTimer(5);
+ connect(m_pluginTimer, &PluginTimer::timeout, this, [this] {
+ foreach(SenseAirS8ModbusRtuConnection *connection, m_s8Connections) {
+ qCDebug(dcSenseAir()) << "Updating...";
+ if (connection->reachable()) {
+ connection->update();
+ }
+ }
+ });
+
+ m_pluginTimer->start();
+
+ }
+}
+
+void IntegrationPluginSenseAir::thingRemoved(Thing *thing)
+{
+ SenseAirS8ModbusRtuConnection *connection = m_s8Connections.take(thing);
+ delete connection;
+
+
+ if (myThings().isEmpty() && m_pluginTimer) {
+ hardwareManager()->pluginTimerManager()->unregisterTimer(m_pluginTimer);
+ m_pluginTimer = nullptr;
+ }
+}
diff --git a/senseair/integrationpluginsenseair.h b/senseair/integrationpluginsenseair.h
new file mode 100644
index 00000000..d4d3cb81
--- /dev/null
+++ b/senseair/integrationpluginsenseair.h
@@ -0,0 +1,64 @@
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+*
+* Copyright 2013 - 2022, nymea GmbH
+* Contact: contact@nymea.io
+*
+* This file is part of nymea.
+* This project including source code and documentation is protected by
+* copyright law, and remains the property of nymea GmbH. All rights, including
+* reproduction, publication, editing and translation, are reserved. The use of
+* this project is subject to the terms of a license agreement to be concluded
+* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
+* under https://nymea.io/license
+*
+* GNU Lesser General Public License Usage
+* Alternatively, this project may be redistributed and/or modified under the
+* terms of the GNU Lesser General Public License as published by the Free
+* Software Foundation; version 3. This project is distributed in the hope that
+* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public License
+* along with this project. If not, see .
+*
+* For any further details and any questions please contact us under
+* contact@nymea.io or see our FAQ/Licensing Information on
+* https://nymea.io/license/faq
+*
+* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+#ifndef INTEGRATIONPLUGINSENSEAIR_H
+#define INTEGRATIONPLUGINSENSEAIR_H
+
+#include
+#include
+
+#include "extern-plugininfo.h"
+#include "senseairs8modbusrtuconnection.h"
+
+class IntegrationPluginSenseAir: public IntegrationPlugin
+{
+ Q_OBJECT
+
+ Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginsenseair.json")
+ Q_INTERFACES(IntegrationPlugin)
+
+public:
+ explicit IntegrationPluginSenseAir();
+
+ void discoverThings(ThingDiscoveryInfo *info) override;
+ void setupThing(ThingSetupInfo *info) override;
+ void postSetupThing(Thing *thing) override;
+ void thingRemoved(Thing *thing) override;
+
+private:
+
+ PluginTimer *m_pluginTimer = nullptr;
+ QHash m_s8Connections;
+
+};
+
+#endif // INTEGRATIONPLUGINSENSEAIR_H
+
+
diff --git a/senseair/integrationpluginsenseair.json b/senseair/integrationpluginsenseair.json
new file mode 100644
index 00000000..1a70518e
--- /dev/null
+++ b/senseair/integrationpluginsenseair.json
@@ -0,0 +1,50 @@
+{
+ "name": "SenseAir",
+ "displayName": "Senseair",
+ "id": "c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38",
+ "vendors": [
+ {
+ "name": "senseAir",
+ "displayName": "SenseAir",
+ "id": "f8427109-ada2-4428-9cfe-61631f763f5a",
+ "thingClasses": [
+ {
+ "name": "s8",
+ "displayName": "SenseAir S8",
+ "id": "b7c80f1c-b583-4e81-a161-c7e1a0b13ea0",
+ "createMethods": ["discovery"],
+ "interfaces": ["co2sensor", "connectable"],
+ "paramTypes": [
+ {
+ "id": "5de7968f-e433-4143-bd0f-b8c3776782b7",
+ "name":"rtuMaster",
+ "displayName": "Modbus RTU master UUID",
+ "type": "QString",
+ "defaultValue": ""
+ }
+ ],
+ "stateTypes": [
+ {
+ "id": "71d6ba02-ca41-41f6-af87-f6e60d371591",
+ "name": "connected",
+ "displayName": "Connected",
+ "type": "bool",
+ "defaultValue": false,
+ "cached": false
+ },
+ {
+ "id": "7506d7c3-ebf7-408b-9625-03be99736a00",
+ "name": "co2",
+ "displayName": "CO2",
+ "type": "double",
+ "unit": "PartsPerMillion",
+ "defaultValue": 0,
+ "minValue": 0,
+ "maxValue": 2000
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/senseair/meta.json b/senseair/meta.json
new file mode 100644
index 00000000..0d860813
--- /dev/null
+++ b/senseair/meta.json
@@ -0,0 +1,13 @@
+{
+ "title": "Senseair",
+ "tagline": "Connect Senseair sensors nymea.",
+ "icon": "senseair.png",
+ "stability": "consumer",
+ "offline": true,
+ "technologies": [
+ "modbus"
+ ],
+ "categories": [
+ "sensor"
+ ]
+}
diff --git a/senseair/s8-registers.json b/senseair/s8-registers.json
new file mode 100644
index 00000000..f9da1f59
--- /dev/null
+++ b/senseair/s8-registers.json
@@ -0,0 +1,31 @@
+{
+ "className": "SenseAirS8",
+ "protocol": "RTU",
+ "errorLimitUntilNotReachable": 20,
+ "endianness": "BigEndian",
+ "checkReachableRegister": "spaceCo2",
+ "registers": [
+ {
+ "id": "meterStatus",
+ "address": 0,
+ "size": 1,
+ "type": "uint16",
+ "readSchedule": "init",
+ "registerType": "inputRegister",
+ "description": "Meter status",
+ "defaultValue": 0,
+ "access": "RO"
+ },
+ {
+ "id": "spaceCo2",
+ "address": 3,
+ "size": 1,
+ "type": "uint16",
+ "readSchedule": "update",
+ "registerType": "inputRegister",
+ "description": "Space CO2",
+ "defaultValue": 0,
+ "access": "RO"
+ }
+ ]
+}
diff --git a/senseair/senseair.png b/senseair/senseair.png
new file mode 100644
index 00000000..30a78553
Binary files /dev/null and b/senseair/senseair.png differ
diff --git a/senseair/senseair.pro b/senseair/senseair.pro
new file mode 100644
index 00000000..c1fb4c0a
--- /dev/null
+++ b/senseair/senseair.pro
@@ -0,0 +1,13 @@
+include(../plugins.pri)
+
+# Generate modbus connection
+MODBUS_CONNECTIONS += s8-registers.json
+MODBUS_TOOLS_CONFIG += VERBOSE
+
+include(../modbus.pri)
+
+HEADERS += \
+ integrationpluginsenseair.h
+
+SOURCES += \
+ integrationpluginsenseair.cpp
diff --git a/senseair/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts b/senseair/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts
new file mode 100644
index 00000000..44fbfcfc
--- /dev/null
+++ b/senseair/translations/c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38-en_US.ts
@@ -0,0 +1,56 @@
+
+
+
+
+ IntegrationPluginSenseAir
+
+
+
+
+
+
+
+
+
+
+
+
+ SenseAir
+
+
+
+ The name of the StateType ({7506d7c3-ebf7-408b-9625-03be99736a00}) of ThingClass s8
+
+
+
+
+
+ The name of the StateType ({71d6ba02-ca41-41f6-af87-f6e60d371591}) of ThingClass s8
+
+
+
+
+
+ The name of the ParamType (ThingClass: s8, Type: thing, ID: {5de7968f-e433-4143-bd0f-b8c3776782b7})
+
+
+
+
+
+ The name of the vendor ({f8427109-ada2-4428-9cfe-61631f763f5a})
+
+
+
+
+
+ The name of the ThingClass ({b7c80f1c-b583-4e81-a161-c7e1a0b13ea0})
+
+
+
+
+
+ The name of the plugin SenseAir ({c7c3c65c-a0cc-4ab1-90d8-4ad05bfcdc38})
+
+
+
+