Merge PR #69: New Plugin: Daylight sensor which works offline and without any hardware

This commit is contained in:
Jenkins 2019-01-17 15:37:21 +01:00
commit af1a5229cc
11 changed files with 435 additions and 22 deletions

View File

@ -332,6 +332,7 @@ void DevicePluginDateTime::processTimesData(const QByteArray &data)
return;
}
qCDebug(dcDateTime()) << "Raw data:" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
QVariantMap response = jsonDoc.toVariant().toMap();
if (response.value("status") != "OK") {
qCWarning(dcDateTime) << "failed to request time data:" << response.value("status");
@ -346,23 +347,36 @@ void DevicePluginDateTime::processTimesData(const QByteArray &data)
QString sunsetString = result.value("sunset").toString();
QString duskString = result.value("civil_twilight_end").toString();
m_dawn = QDateTime(QDate::currentDate(), QTime::fromString(dawnString, "h:m:s AP"), Qt::UTC).toTimeZone(m_timeZone);
m_sunrise = QDateTime(QDate::currentDate(), QTime::fromString(sunriseString, "h:m:s AP"), Qt::UTC).toTimeZone(m_timeZone);
m_noon = QDateTime(QDate::currentDate(), QTime::fromString(noonString, "h:m:s AP"), Qt::UTC).toTimeZone(m_timeZone);
m_sunset = QDateTime(QDate::currentDate(), QTime::fromString(sunsetString, "h:m:s AP"), Qt::UTC).toTimeZone(m_timeZone);
m_dusk = QDateTime(QDate::currentDate(), QTime::fromString(duskString, "h:m:s AP"), Qt::UTC).toTimeZone(m_timeZone);
// calculate the times in each alarm
// qCDebug(dcDateTime) << " dawn :" << m_dawn.toString();
// qCDebug(dcDateTime) << " sunrise :" << m_sunrise.toString();
// qCDebug(dcDateTime) << " noon :" << m_noon.toString();
// qCDebug(dcDateTime) << " sunset :" << m_sunset.toString();
// qCDebug(dcDateTime) << " dusk :" << m_dusk.toString();
// qCDebug(dcDateTime) << "---------------------------------------------";
m_dawn = QDateTime(QDate::currentDate(), parseTime(dawnString), Qt::UTC).toTimeZone(m_timeZone);
m_sunrise = QDateTime(QDate::currentDate(), parseTime(sunriseString), Qt::UTC).toTimeZone(m_timeZone);
m_noon = QDateTime(QDate::currentDate(), parseTime(noonString), Qt::UTC).toTimeZone(m_timeZone);
m_sunset = QDateTime(QDate::currentDate(), parseTime(sunsetString), Qt::UTC).toTimeZone(m_timeZone);
m_dusk = QDateTime(QDate::currentDate(), parseTime(duskString), Qt::UTC).toTimeZone(m_timeZone);
qCDebug(dcDateTime) << " dawn :" << m_dawn.toString() << dawnString;
qCDebug(dcDateTime) << " sunrise :" << m_sunrise.toString() << sunriseString;
qCDebug(dcDateTime) << " noon :" << m_noon.toString() << noonString;
qCDebug(dcDateTime) << " sunset :" << m_sunset.toString() << sunsetString;
qCDebug(dcDateTime) << " dusk :" << m_dusk.toString() << duskString;
qCDebug(dcDateTime) << "---------------------------------------------";
updateTimes();
}
QTime DevicePluginDateTime::parseTime(const QString &timeString) const
{
bool isPm = timeString.endsWith(" PM");
QString tmp = QString(timeString).remove(QRegExp("[ APM]*"));
QStringList parts = tmp.split(":");
if (parts.count() != 3) {
qCWarning(dcDateTime()) << "Error parsing timeString:" << timeString;
return QTime();
}
return QTime(parts.first().toInt(), parts.at(1).toInt(), parts.last().toInt()).addSecs(isPm ? 60 * 60 * 12 : 0);
}
void DevicePluginDateTime::onNetworkReplayFinished()
{
QNetworkReply *reply = static_cast<QNetworkReply *>(sender());
@ -490,21 +504,20 @@ void DevicePluginDateTime::updateTimes()
} else {
m_todayDevice->setStateValue(todayDuskStateTypeId, 0);
}
if (m_dusk.isValid()) {
m_todayDevice->setStateValue(todaySunriseStateTypeId, m_sunrise.toTime_t());
if (m_sunrise.isValid() && m_sunset.isValid()) {
m_todayDevice->setStateValue(todaySunriseTimeStateTypeId, m_sunrise.toTime_t());
m_todayDevice->setStateValue(todaySunsetTimeStateTypeId, m_sunset.toTime_t());
m_todayDevice->setStateValue(todayDaylightStateTypeId, m_sunrise < m_currentDateTime && m_currentDateTime < m_sunset);
} else {
m_todayDevice->setStateValue(todaySunriseStateTypeId, 0);
m_todayDevice->setStateValue(todaySunriseTimeStateTypeId, 0);
m_todayDevice->setStateValue(todaySunsetTimeStateTypeId, 0);
m_todayDevice->setStateValue(todayDaylightStateTypeId, false);
}
if (m_dusk.isValid()) {
m_todayDevice->setStateValue(todayNoonStateTypeId, m_noon.toTime_t());
} else {
m_todayDevice->setStateValue(todayNoonStateTypeId, 0);
}
if (m_dusk.isValid()) {
m_todayDevice->setStateValue(todaySunsetStateTypeId, m_sunset.toTime_t());
} else {
m_todayDevice->setStateValue(todaySunsetStateTypeId, 0);
}
if (m_dusk.isValid()) {
m_todayDevice->setStateValue(todayDawnStateTypeId, m_dawn.toTime_t());
} else {

View File

@ -75,6 +75,8 @@ private:
void getTimes(const QString &latitude, const QString &longitude);
void processTimesData(const QByteArray &data);
QTime parseTime(const QString &timeString) const;
signals:
void dusk();
void sunset();

View File

@ -13,6 +13,7 @@
"name": "today",
"displayName": "Today",
"deviceIcon": "Time",
"interfaces": [ "daylightsensor" ],
"basicTags": [
"Service",
"Time"
@ -133,7 +134,7 @@
},
{
"id": "3a08824d-285b-412e-a515-9664b491a85c",
"name": "sunrise",
"name": "sunriseTime",
"displayName": "sunrise",
"displayNameEvent": "sunrise changed",
"unit": "UnixTime",
@ -163,13 +164,21 @@
},
{
"id": "377f04a7-df58-42ad-a234-e9e23bdc2f85",
"name": "sunset",
"name": "sunsetTime",
"displayName": "sunset",
"displayNameEvent": "sunset changed",
"unit": "UnixTime",
"type": "int",
"eventRuleRelevant": false,
"defaultValue": 0
},
{
"id": "1c3d6179-3b00-456c-841a-2d26ce960c25",
"name": "daylight",
"displayName": "Daylight",
"displayNameEvent": "Daylight changed",
"type": "bool",
"defaultValue": false
}
],
"eventTypes": [

View File

@ -0,0 +1,11 @@
include(../plugins.pri)
QT += network
TARGET = $$qtLibraryTarget(nymea_deviceplugindaylightsensor)
SOURCES += \
deviceplugindaylightsensor.cpp \
HEADERS += \
deviceplugindaylightsensor.h \

View File

@ -0,0 +1,233 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library 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 library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "plugininfo.h"
#include "devicemanager.h"
#include "deviceplugindaylightsensor.h"
#include "network/networkaccessmanager.h"
#include <QUrlQuery>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QtMath>
#include <QPair>
#include <QTimeZone>
#define CIVIL_ZENITH 90.83333
DevicePluginDaylightSensor::DevicePluginDaylightSensor()
{
}
DevicePluginDaylightSensor::~DevicePluginDaylightSensor()
{
}
DeviceManager::DeviceError DevicePluginDaylightSensor::discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params)
{
Q_UNUSED(deviceClassId)
Q_UNUSED(params)
QNetworkRequest request(QUrl("http://ip-api.com/json"));
QNetworkReply* reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, deviceClassId]() {
reply->deleteLater();
QList<DeviceDescriptor> results;
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcDaylightSensor()) << "Error fetching geolocation from ip-api:" << reply->error() << reply->errorString();
emit devicesDiscovered(deviceClassId, results);
return;
}
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcDaylightSensor()) << "Failed to parse json from ip-api:" << error.error << error.errorString();
emit devicesDiscovered(deviceClassId, results);
return;
}
if (!jsonDoc.toVariant().toMap().contains("lat") || !jsonDoc.toVariant().toMap().contains("lon")) {
qCWarning(dcDaylightSensor()) << "Reply missing geolocation info" << qUtf8Printable(jsonDoc.toJson(QJsonDocument::Indented));
emit devicesDiscovered(deviceClassId, results);
return;
}
qreal lat = jsonDoc.toVariant().toMap().value("lat").toDouble();
qreal lon = jsonDoc.toVariant().toMap().value("lon").toDouble();
DeviceDescriptor descriptor(deviceClassId, tr("Daylight sensor"), jsonDoc.toVariant().toMap().value("city").toString());
ParamList params;
params.append(Param(daylightSensorDeviceLatitudeParamTypeId, lat));
params.append(Param(daylightSensorDeviceLongitudeParamTypeId, lon));
descriptor.setParams(params);
results.append(descriptor);
emit devicesDiscovered(deviceClassId, results);
});
return DeviceManager::DeviceErrorAsync;
}
DeviceManager::DeviceSetupStatus DevicePluginDaylightSensor::setupDevice(Device *device)
{
updateDevice(device);
return DeviceManager::DeviceSetupStatusSuccess;
}
void DevicePluginDaylightSensor::deviceRemoved(Device *device)
{
hardwareManager()->pluginTimerManager()->unregisterTimer(m_timers.take(device));
}
void DevicePluginDaylightSensor::pluginTimerEvent()
{
PluginTimer *timer = static_cast<PluginTimer*>(sender());
Device *device = m_timers.key(timer);
if (!myDevices().contains(device)) {
qCDebug(dcDaylightSensor()) << "Device has disappeared. Exiting timer loop.";
return;
}
updateDevice(device);
}
void DevicePluginDaylightSensor::updateDevice(Device *device)
{
PluginTimer *timer = m_timers.value(device);
if (timer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(timer);
}
QTimeZone tz = QTimeZone(QTimeZone::systemTimeZoneId());
QDateTime now = QDateTime::currentDateTime().toTimeZone(tz);
auto sunriseSunset = calculateSunriseSunset(device->paramValue(daylightSensorDeviceLatitudeParamTypeId).toDouble(), device->paramValue(daylightSensorDeviceLongitudeParamTypeId).toDouble(), now);
QDateTime sunrise = sunriseSunset.first.toTimeZone(tz);
QDateTime sunset = sunriseSunset.second.toTimeZone(tz);
qCDebug(dcDaylightSensor()) << "Setting up daylight sensor:" << device->name() << "Sunrise:" << sunrise.toString() << "Sunset:" << sunset.toString();
device->setStateValue(daylightSensorSunriseTimeStateTypeId, sunrise.toTime_t());
device->setStateValue(daylightSensorSunsetTimeStateTypeId, sunset.toTime_t());
device->setStateValue(daylightSensorDaylightStateTypeId, sunrise < now && sunset > now);
qint64 timeToNext = -1;
if (now < sunrise) {
timeToNext = now.secsTo(sunrise);
} else if (now < sunset) {
timeToNext = now.secsTo(sunset);
} else {
timeToNext = (60 * 60 * 24) - (now.time().msecsSinceStartOfDay() / 1000);
}
// Refresh at earliest in 5 secs to avoid spamming the system when we get close
timeToNext = qMax(static_cast<int>(timeToNext), 1);
timer = hardwareManager()->pluginTimerManager()->registerTimer(static_cast<int>(timeToNext));
qCDebug(dcDaylightSensor()) << "Recalculating in" << timer->interval() << "seconds";
connect(timer, &PluginTimer::timeout, this, &DevicePluginDaylightSensor::pluginTimerEvent);
m_timers.insert(device, timer);
}
QPair<QDateTime, QDateTime> DevicePluginDaylightSensor::calculateSunriseSunset(qreal latitude, qreal longitude, const QDateTime &dateTime)
{
int dayOfYear = dateTime.date().dayOfYear();
int offset = dateTime.offsetFromUtc() / 60 / 60;
// Convert the longitude to hour value and calculate an approximate time
qreal longitudeHour = longitude / 15;
qreal tRise = dayOfYear + ((6 - longitudeHour) / 24);
qreal tSet = dayOfYear + ((18 - longitudeHour) / 24);
// Calculate the Sun's mean anomaly
qreal mRise = (0.9856 * tRise) - 3.289;
qreal mSet = (0.9856 * tSet) - 3.289;
// Calculate the Sun's true longitude, and adjust angle to be between 0 and 360
qreal tmp = mRise + (1.916 * qSin(qDegreesToRadians(mRise))) + (0.020 * qSin(qDegreesToRadians(2 * mRise))) + 282.634;
qreal lRise = qFloor(tmp + 360) % 360 + (tmp - qFloor(tmp));
tmp = mSet + (1.916 * qSin(qDegreesToRadians(mSet))) + (0.020 * qSin(qDegreesToRadians(2 * mSet))) + 282.634;
qreal lSet = qFloor(tmp + 360) % 360 + (tmp - qFloor(tmp));
// Calculate the Sun's right ascension, and adjust angle to be between 0 and 360
tmp = qRadiansToDegrees(qAtan(0.91764 * qTan(qDegreesToRadians(1.0 * lRise))));
qreal raRise = qFloor(tmp + 360) % 360 + (tmp - qFloor(tmp));
tmp = qRadiansToDegrees(qAtan(0.91764 * qTan(qDegreesToRadians(1.0 * lSet))));
qreal raSet = qRound(tmp + 360) % 360 + (tmp - qFloor(tmp));
// Right ascension value needs to be in the same quadrant as L
qlonglong lQuadrantRise = qFloor(lRise/90) * 90;
qlonglong raQuadrantRise = qFloor(raRise/90) * 90;
raRise = raRise + (lQuadrantRise - raQuadrantRise);
qlonglong lQuadrantSet = qFloor(lSet/90) * 90;
qlonglong raQuadrantSet = qFloor(raSet/90) * 90;
raSet = raSet + (lQuadrantSet - raQuadrantSet);
// Right ascension value needs to be converted into hours
raRise = raRise / 15;
raSet = raSet / 15;
// Calculate the Sun's declination
qreal sinDecRise = 0.39782 * qSin(qDegreesToRadians(1.0 * lRise));
qreal cosDecRise = qCos(qAsin(sinDecRise));
qreal sinDecSet = 0.39782 * qSin(qDegreesToRadians(1.0 * lSet));
qreal cosDecSet = qCos(qAsin(sinDecSet));
// Calculate the Sun's local hour angle
qreal cosZenith = qCos(qDegreesToRadians(CIVIL_ZENITH));
qreal radianLat = qDegreesToRadians(latitude);
qreal sinLatitude = qSin(radianLat);
qreal cosLatitude = qCos(radianLat);
qreal cosHRise = (cosZenith - (sinDecRise * sinLatitude)) / (cosDecRise * cosLatitude);
qreal cosHSet = (cosZenith - (sinDecSet * sinLatitude)) / (cosDecSet * cosLatitude);
// Finish calculating H and convert into hours
qreal hRise = (360 - qRadiansToDegrees(qAcos(cosHRise))) / 15;
qreal hSet = qRadiansToDegrees(qAcos(cosHSet)) / 15;
// Calculate local mean time of rising/setting
tRise = hRise + raRise - (0.06571 * tRise) - 6.622;
tSet = hSet + raSet - (0.06571 * tSet) - 6.622;
// Adjust back to UTC, and keep the time between 0 and 24
tmp = tRise - longitudeHour;
qreal utRise = qFloor(tmp + 24) % 24 + (tmp - qFloor(tmp));
tmp = tSet - longitudeHour;
qreal utSet = qFloor(tmp + 24) % 24 + (tmp - qFloor(tmp));
// Adjust again to localtime
tmp = utRise + offset;
qreal localtRise = qFloor(tmp + 24) % 24 + (tmp - qFloor(tmp));
tmp = utSet + offset;
qreal localtSet = qFloor(tmp + 24) % 24 + (tmp - qFloor(tmp));
// Conversion
int hourRise = qFloor(localtRise);
int minuteRise = qFloor((localtRise - qFloor(localtRise)) * 60);
int hourSet = qFloor(localtSet);
int minuteSet = qFloor((localtSet - qFloor(localtSet)) * 60);
QDateTime sunrise(dateTime.date(), QTime(hourRise, minuteRise));
QDateTime sunset(dateTime.date(), QTime(hourSet, minuteSet));
return qMakePair<QDateTime, QDateTime>(sunrise, sunset);
}

View File

@ -0,0 +1,55 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library 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 library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef DEVICEPLUGINDAYLIGHTSENSOR_H
#define DEVICEPLUGINDAYLIGHTSENSOR_H
#include "plugin/deviceplugin.h"
#include "devicemanager.h"
#include "plugintimer.h"
class DevicePluginDaylightSensor: public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugindaylightsensor.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginDaylightSensor();
~DevicePluginDaylightSensor() override;
DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params) override;
DeviceManager::DeviceSetupStatus setupDevice(Device *device) override;
void deviceRemoved(Device *device) override;
private slots:
void pluginTimerEvent();
void updateDevice(Device *device);
private:
QHash<Device*, PluginTimer*> m_timers;
QPair<QDateTime, QDateTime> calculateSunriseSunset(qreal lat, qreal lon, const QDateTime &dateTime);
};
#endif // DEVICEPLUGINDAYLIGHTSENSOR_H

View File

@ -0,0 +1,66 @@
{
"displayName": "Daylight sensor",
"name": "daylightSensor",
"id": "4b7d63a7-fc39-4a50-a459-457fa7653089",
"vendors": [
{
"name": "guh",
"displayName": "guh GmbH",
"id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6",
"deviceClasses": [
{
"id": "1d43d0ed-efe2-46ab-a567-af632e550931",
"name": "daylightSensor",
"displayName": "Daylight sensor",
"createMethods": ["user", "discovery"],
"interfaces": ["daylightsensor"],
"paramTypes": [
{
"id": "124b2574-8f57-4c62-bc55-40e467d4aea3",
"name": "latitude",
"displayName": "Latitude",
"type": "QString",
"inputType": "TextLine"
},
{
"id": "581c6914-8dbf-4882-a38c-453b808db374",
"name": "longitude",
"displayName": "Longitude",
"type": "QString",
"inputType": "TextLine"
}
],
"stateTypes": [
{
"id": "6e7bc630-a19c-41ec-b6a3-2e0cbcc4fadf",
"name": "daylight",
"displayName": "Daylight",
"displayNameEvent": "Daylight changed",
"type": "bool",
"cached": false,
"defaultValue": false
},
{
"id": "d7c1f356-1efe-4ab7-ad4b-e7ab12cb53b7",
"name": "sunriseTime",
"displayName": "Sunrise time",
"displayNameEvent": "Sunrise time changed",
"unit": "UnixTime",
"type": "int",
"defaultValue": 0
},
{
"id": "6d064789-b9c3-44c8-a5ea-b722fc2f56b7",
"name": "sunsetTime",
"displayName": "Sunset time",
"displayNameEvent": "Sunset time changed",
"unit": "UnixTime",
"type": "int",
"defaultValue": 0
}
]
}
]
}
]
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
</TS>

18
debian/control vendored
View File

@ -129,6 +129,23 @@ Description: nymea.io plugin for datetime
This package will install the nymea.io plugin for datetime
Package: nymea-plugin-daylightsensor
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
nymea-plugins-translations,
Description: nymea.io plugin for a mathematical daylight sensor
The nymea daemon is a plugin based IoT (Internet of Things) server. The
server works like a translator for devices, things and services and
allows them to interact.
With the powerful rule engine you are able to connect any device available
in the system and create individual scenes and behaviors for your environment.
.
This package will install a plugin for a mathematical daylight sensor. That means
it will work without any internet connection or hardware. It computes daylight
times based on the geographical location of the nymea instance.
Package: nymea-plugin-denon
Architecture: any
Depends: ${shlibs:Depends},
@ -752,6 +769,7 @@ Section: libs
Architecture: all
Depends: nymea-plugin-awattar,
nymea-plugin-datetime,
nymea-plugin-daylightsensor,
nymea-plugin-denon,
nymea-plugin-eq-3,
nymea-plugin-flowercare,

View File

@ -0,0 +1 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_deviceplugindaylightsensor.so

View File

@ -8,6 +8,7 @@ PLUGIN_DIRS = \
commandlauncher \
conrad \
datetime \
daylightsensor \
denon \
dweetio \
elgato \