powersync-plugins/doorbird/deviceplugindoorbird.cpp

217 lines
10 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2019 Bernhard Trinnes <bernhard.trinnes@nymea.io> *
* Copyright (C) 2019 Michael Zanetti <michael.zanetti@nymea.io> *
* *
* This file is part of nymea. *
* *
* nymea is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, version 2 of the License. *
* *
* nymea 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with nymea. If not, see <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "deviceplugindoorbird.h"
#include "plugininfo.h"
#include "platform/platformzeroconfcontroller.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include "network/zeroconf/zeroconfserviceentry.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QTimer>
DevicePluginDoorbird::DevicePluginDoorbird()
{
m_nam = new QNetworkAccessManager(this);
connect(m_nam, &QNetworkAccessManager::authenticationRequired, this, [this](QNetworkReply *reply, QAuthenticator *authenticator) {
Device *dev = m_networkRequests.value(reply);
if (!myDevices().contains(dev)) {
qCWarning(dcDoorBird) << "Credentials requested for a device which doesn't exist any more";
return;
}
qCDebug(dcDoorBird) << "Credentials requested for device:" << dev->name();
authenticator->setUser(dev->paramValue(doorBirdDeviceUsernameParamTypeId).toString());
authenticator->setPassword(dev->paramValue(doorBirdDevicePasswordParamTypeId).toString());
});
}
DevicePluginDoorbird::~DevicePluginDoorbird()
{
}
Device::DeviceError DevicePluginDoorbird::discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params)
{
Q_UNUSED(deviceClassId)
Q_UNUSED(params)
// NOTE: Discovery is currently disabled in json file because we don't support discovery & login as parameters in combination
// and there isn't any setupMethod which would allow us to enter user & password.
ZeroConfServiceBrowser *serviceBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_xbmc-jsonrpc._tcp");
QTimer::singleShot(5000, this, [this, serviceBrowser](){
QList<DeviceDescriptor> deviceDescriptors;
foreach (const ZeroConfServiceEntry serviceEntry, serviceBrowser->serviceEntries()) {
if (serviceEntry.serviceType() == "_axis-video._tcp" && serviceEntry.hostName().startsWith("bha-")) {
qCDebug(dcDoorBird) << "Found DoorBird device";
DeviceDescriptor deviceDescriptor(doorBirdDeviceClassId, serviceEntry.name(), serviceEntry.hostAddress().toString());
ParamList params;
//TODO add rediscovery
params.append(Param(doorBirdDeviceAddressParamTypeId, serviceEntry.hostAddress().toString()));
deviceDescriptor.setParams(params);
deviceDescriptors.append(deviceDescriptor);
}
}
emit devicesDiscovered(doorBirdDeviceClassId, deviceDescriptors);
serviceBrowser->deleteLater();
});
return Device::DeviceErrorAsync;
}
Device::DeviceSetupStatus DevicePluginDoorbird::setupDevice(Device *device)
{
connectToEventMonitor(device);
return Device::DeviceSetupStatusSuccess;
}
Device::DeviceError DevicePluginDoorbird::executeAction(Device *device, const Action &action)
{
if (action.actionTypeId() == doorBirdUnlatchActionTypeId) {
QNetworkRequest request(QString("http://%1/bha-api/open-door.cgi?r=1").arg(device->paramValue(doorBirdDeviceAddressParamTypeId).toString()));
qCDebug(dcDoorBird) << "Sending request:" << request.url();
QNetworkReply *reply = m_nam->get(request);
m_networkRequests.insert(reply, device);
connect(reply, &QNetworkReply::finished, this, [this, reply, device, action](){
reply->deleteLater();
m_networkRequests.remove(reply);
if (!myDevices().contains(device)) {
// Device must have been removed in the meantime
return;
}
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcDoorBird) << "Error unlatching DoorBird device" << device->name();
emit actionExecutionFinished(action.id(), Device::DeviceErrorHardwareFailure);
return;
}
qCDebug(dcDoorBird) << "DoorBird unlatched:" << reply->error() << reply->errorString();
emit actionExecutionFinished(action.id(), Device::DeviceErrorNoError);
});
}
return Device::DeviceErrorDeviceClassNotFound;
}
void DevicePluginDoorbird::connectToEventMonitor(Device *device)
{
qCDebug(dcDoorBird) << "Starting monitoring" << device->name();
QNetworkRequest request(QString("http://%1/bha-api/monitor.cgi?ring=doorbell,motionsensor").arg(device->paramValue(doorBirdDeviceAddressParamTypeId).toString()));
QNetworkReply *reply = m_nam->get(request);
m_networkRequests.insert(reply, device);
connect(reply, &QNetworkReply::downloadProgress, this, [this, device, reply](qint64 bytesReceived, qint64 bytesTotal){
Q_UNUSED(bytesReceived)
Q_UNUSED(bytesTotal);
if (!myDevices().contains(device)) {
qCWarning(dcDoorBird) << "Device disappeared for monitor stream.";
reply->abort();
return;
}
device->setStateValue(doorBirdConnectedStateTypeId, true);
m_readBuffers[device].append(reply->readAll());
// qCDebug(dcDoorBird) << "Monitor data for" << device->name();
// qCDebug(dcDoorBird) << m_readBuffers[device];
// Input data looks like:
// "--ioboundary\r\nContent-Type: text/plain\r\n\r\ndoorbell:H\r\n\r\n"
while (!m_readBuffers[device].isEmpty()) {
// find next --ioboundary
QString boundary = QStringLiteral("--ioboundary");
int startIndex = m_readBuffers[device].indexOf(boundary);
if (startIndex == -1) {
qCWarning(dcDoorBird) << "No meaningful data in buffer:" << m_readBuffers[device];
if (m_readBuffers[device].size() > 1024) {
qCWarning(dcDoorBird) << "Buffer size > 1KB and still no meaningful data. Discarding buffer...";
m_readBuffers[device].clear();
}
// Assuming we don't have enough data yet...
return;
}
QByteArray contentType = QByteArrayLiteral("Content-Type: text/plain");
int contentTypeIndex = m_readBuffers[device].indexOf(contentType);
if (contentTypeIndex == -1) {
qCWarning(dcDoorBird) << "Cannot find Content-Type in buffer:" << m_readBuffers[device];
if (m_readBuffers[device].size() > startIndex + 50) {
qCWarning(dcDoorBird) << boundary << "found but unexpected data follows. Skipping this element...";
m_readBuffers[device].remove(0, startIndex + boundary.length());
continue;
}
// Assuming we don't have enough data yet...
return;
}
// At this point we have the boundary and Content-Type. Remove all of that and take the entire string to either end or next boundary
m_readBuffers[device].remove(0, contentTypeIndex + contentType.length());
int nextStartIndex = m_readBuffers[device].indexOf(boundary);
QByteArray data;
if (nextStartIndex == -1) {
data = m_readBuffers[device];
m_readBuffers[device].clear();
} else {
data = m_readBuffers[device].left(nextStartIndex);
m_readBuffers[device].remove(0, nextStartIndex);
}
QString message = data.trimmed();
QStringList parts = message.split(":");
if (parts.count() != 2) {
qCWarning(dcDoorBird) << "Message has invalid format:" << message << "Expected device:state";
continue;
}
if (parts.first() == "doorbell") {
if (parts.at(1) == "H") {
qCDebug(dcDoorBird) << "Doorbell ringing!";
emitEvent(Event(doorBirdTriggeredEventTypeId, device->id()));
}
} else if (parts.first() == "motionsensor") {
if (parts.at(1) == "H") {
qCDebug(dcDoorBird) << "Motion sensor detected a person";
emitEvent(Event(doorBirdMotionDetectedEventTypeId, device->id()));
}
} else {
qCWarning(dcDoorBird) << "Unhandled DoorBird data:" << message;
}
}
});
connect(reply, &QNetworkReply::finished, this, [this, device, reply](){
reply->deleteLater();
m_networkRequests.remove(reply);
if (!myDevices().contains(device)) {
qCWarning(dcDoorBird) << "Device has disappeared. Exiting monitor.";
return;
}
device->setStateValue(doorBirdConnectedStateTypeId, false);
qCDebug(dcDoorBird) << "Monitor request finished:" << reply->error();
QTimer::singleShot(2000, this, [this, device] {
if (!myDevices().contains(device)) {
return;
}
connectToEventMonitor(device);
});
});
}