diff --git a/doorbird/deviceplugindoorbird.cpp b/doorbird/deviceplugindoorbird.cpp new file mode 100644 index 00000000..e62e4cf7 --- /dev/null +++ b/doorbird/deviceplugindoorbird.cpp @@ -0,0 +1,216 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Bernhard Trinnes * + * Copyright (C) 2019 Michael Zanetti * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "deviceplugindoorbird.h" +#include "plugininfo.h" + +#include "platform/platformzeroconfcontroller.h" +#include "network/zeroconf/zeroconfservicebrowser.h" +#include "network/zeroconf/zeroconfserviceentry.h" + +#include +#include +#include +#include + +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 ¶ms) +{ + 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 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); + }); + }); +} diff --git a/doorbird/deviceplugindoorbird.h b/doorbird/deviceplugindoorbird.h new file mode 100644 index 00000000..89edfb46 --- /dev/null +++ b/doorbird/deviceplugindoorbird.h @@ -0,0 +1,56 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * * + * Copyright (C) 2019 Michael Zanetti * + * * + * 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 . * + * * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef DEVICEPLUGINDOORBIRD_H +#define DEVICEPLUGINDOORBIRD_H + +#include "devices/deviceplugin.h" +#include "devices/devicemanager.h" + +class QNetworkAccessManager; +class QNetworkReply; + +class DevicePluginDoorbird: public DevicePlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "deviceplugindoorbird.json") + Q_INTERFACES(DevicePlugin) + + +public: + explicit DevicePluginDoorbird(); + ~DevicePluginDoorbird() override; + + Device::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList ¶ms) override; + + Device::DeviceSetupStatus setupDevice(Device *device) override; + Device::DeviceError executeAction(Device *device, const Action &action) override; + + void connectToEventMonitor(Device *device); +private: + + QNetworkAccessManager *m_nam = nullptr; + + QHash m_networkRequests; + QHash m_readBuffers; +}; + +#endif // DEVICEPLUGINDOORBIRD_H diff --git a/doorbird/deviceplugindoorbird.json b/doorbird/deviceplugindoorbird.json new file mode 100644 index 00000000..974da11a --- /dev/null +++ b/doorbird/deviceplugindoorbird.json @@ -0,0 +1,70 @@ +{ + "name": "doorBird", + "displayName": "DoorBird", + "id": "6fe1614a-fc47-4eb2-a47c-13c50f1798ee", + "vendors": [ + { + "name": "doorBird", + "displayName": "DoorBird", + "id": "2da07435-571e-4956-a387-6caa51d6e845", + "deviceClasses": [ + { + "id": "0485eb61-2a22-42ba-9dd2-a5961485bf08", + "name": "doorBird", + "displayName": "DoorBird", + "createMethods": ["discovery", "user" ], + "interfaces": [ "inputtrigger", "connectable" ], + "paramTypes": [ + { + "id": "8873b17d-526e-408d-95d8-6439b501f489", + "name": "address", + "displayName": "IP address", + "type": "QString" + }, + { + "id": "7ccd8f3a-2a5f-4b90-8042-92899d0ee32a", + "name": "username", + "displayName": "Username", + "type": "QString" + }, + { + "id": "ea285a57-47c5-43f1-b0d6-e0a4d6230f3c", + "name": "password", + "displayName": "Password", + "type": "QString" + } + ], + "actionTypes": [ + { + "id": "b6c3377b-91de-411a-9d48-8b509c39d67c", + "name": "unlatch", + "displayName": "Unlatch the door" + } + ], + "stateTypes": [ + { + "id": "186c270b-923c-46e4-a7da-33e45427cdbb", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "defaultValue": false + } + ], + "eventTypes": [ + { + "id": "9bc89937-a2ab-4e8e-af0e-a9ba41caa89b", + "name": "triggered", + "displayName": "Doorbell pressed" + }, + { + "id": "e9bb229b-8776-4110-a813-9c0dc67375db", + "name": "motionDetected", + "displayName": "Motion detected" + } + ] + } + ] + } + ] +} diff --git a/doorbird/doorbird.pro b/doorbird/doorbird.pro new file mode 100644 index 00000000..c10ea83a --- /dev/null +++ b/doorbird/doorbird.pro @@ -0,0 +1,11 @@ +include(../plugins.pri) + +QT += network + +TARGET = $$qtLibraryTarget(nymea_deviceplugindoorbird) + +SOURCES += \ + deviceplugindoorbird.cpp \ + +HEADERS += \ + deviceplugindoorbird.h \ diff --git a/doorbird/translations/6fe1614a-fc47-4eb2-a47c-13c50f1798ee-en_US.ts b/doorbird/translations/6fe1614a-fc47-4eb2-a47c-13c50f1798ee-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/doorbird/translations/6fe1614a-fc47-4eb2-a47c-13c50f1798ee-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/doorbird/translations/b7368429-e312-4c82-9eab-e1cd996e43d6-en_US.ts b/doorbird/translations/b7368429-e312-4c82-9eab-e1cd996e43d6-en_US.ts new file mode 100644 index 00000000..f7f66d85 --- /dev/null +++ b/doorbird/translations/b7368429-e312-4c82-9eab-e1cd996e43d6-en_US.ts @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 23d78cdf..f20cccb2 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -12,6 +12,7 @@ PLUGIN_DIRS = \ datetime \ daylightsensor \ denon \ + doorbird \ dweetio \ elgato \ elro \