mirror of
https://github.com/nymea/nymea-plugins.git
synced 2026-07-15 00:22:26 +02:00
WIP: New Plugin: Nymea owlets
This commit is contained in:
parent
2bb91cf98b
commit
3985f2ad5a
239
owlet/integrationpluginowlet.cpp
Normal file
239
owlet/integrationpluginowlet.cpp
Normal file
@ -0,0 +1,239 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2021, 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 "integrationpluginowlet.h"
|
||||
#include "plugininfo.h"
|
||||
#include "owletclient.h"
|
||||
|
||||
#include "hardwaremanager.h"
|
||||
#include "platform/platformzeroconfcontroller.h"
|
||||
#include "network/zeroconf/zeroconfservicebrowser.h"
|
||||
#include "network/zeroconf/zeroconfserviceentry.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QTimer>
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> idParamTypeMap = {
|
||||
{ digitalOutputThingClassId, digitalOutputThingOwletIdParamTypeId },
|
||||
{ digitalInputThingClassId, digitalInputThingOwletIdParamTypeId },
|
||||
{ ws2812ThingClassId, ws2812ThingOwletIdParamTypeId }
|
||||
};
|
||||
|
||||
IntegrationPluginOwlet::IntegrationPluginOwlet()
|
||||
{
|
||||
}
|
||||
|
||||
void IntegrationPluginOwlet::init()
|
||||
{
|
||||
m_zeroConfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_nymea-owlet._tcp");
|
||||
}
|
||||
|
||||
void IntegrationPluginOwlet::discoverThings(ThingDiscoveryInfo *info)
|
||||
{
|
||||
foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) {
|
||||
qCDebug(dcOwlet()) << "Found owlet:" << entry;
|
||||
ThingDescriptor descriptor(info->thingClassId(), entry.name(), entry.txt("platform"));
|
||||
descriptor.setParams(ParamList() << Param(idParamTypeMap.value(info->thingClassId()), entry.txt("id")));
|
||||
foreach (Thing *existingThing, myThings().filterByParam(idParamTypeMap.value(info->thingClassId()), entry.txt("id"))) {
|
||||
descriptor.setThingId(existingThing->id());
|
||||
break;
|
||||
}
|
||||
info->addThingDescriptor(descriptor);
|
||||
}
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
}
|
||||
|
||||
|
||||
void IntegrationPluginOwlet::setupThing(ThingSetupInfo *info)
|
||||
{
|
||||
Thing *thing = info->thing();
|
||||
|
||||
QHostAddress ip;
|
||||
int port = 5555;
|
||||
foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) {
|
||||
if (entry.txt("id") == info->thing()->paramValue(idParamTypeMap.value(info->thing()->thingClassId()))) {
|
||||
ip = entry.hostAddress();
|
||||
port = entry.port();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Try cached ip
|
||||
if (ip.isNull()) {
|
||||
pluginStorage()->beginGroup(thing->id().toString());
|
||||
ip = QHostAddress(pluginStorage()->value("cachedIP").toString());
|
||||
pluginStorage()->endGroup();
|
||||
}
|
||||
|
||||
if (ip.isNull()) {
|
||||
qCWarning(dcOwlet()) << "Can't find owlet in the local network.";
|
||||
info->finish(Thing::ThingErrorHardwareNotAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
OwletClient *client = new OwletClient(this);
|
||||
|
||||
connect(client, &OwletClient::connected, info, [=](){
|
||||
qCDebug(dcOwlet()) << "Connected to owleet";
|
||||
m_clients.insert(thing, client);
|
||||
|
||||
if (thing->thingClassId() == digitalOutputThingClassId) {
|
||||
QVariantMap params;
|
||||
params.insert("id", thing->paramValue(digitalOutputThingPinParamTypeId).toInt());
|
||||
params.insert("mode", "GPIOOutput");
|
||||
client->sendCommand("GPIO.ConfigurePin", params);
|
||||
}
|
||||
if (thing->thingClassId() == digitalInputThingClassId) {
|
||||
QVariantMap params;
|
||||
params.insert("id", thing->paramValue(digitalInputThingPinParamTypeId).toInt());
|
||||
params.insert("mode", "GPIOInput");
|
||||
client->sendCommand("GPIO.ConfigurePin", params);
|
||||
}
|
||||
if (thing->thingClassId() == ws2812ThingClassId) {
|
||||
QVariantMap params;
|
||||
params.insert("id", thing->paramValue(ws2812ThingPinParamTypeId).toInt());
|
||||
params.insert("mode", "WS2812");
|
||||
params.insert("ledCount", thing->paramValue(ws2812ThingLedCountParamTypeId).toUInt());
|
||||
params.insert("ledMode", "WS2812Mode" + thing->paramValue(ws2812ThingLedModeParamTypeId).toString());
|
||||
params.insert("ledClock", "WS2812Clock" + thing->paramValue(ws2812ThingLedClockParamTypeId).toString());
|
||||
client->sendCommand("GPIO.ConfigurePin", params);
|
||||
}
|
||||
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
});
|
||||
connect(client, &OwletClient::error, info, [=](){
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
});
|
||||
connect(client, &OwletClient::connected, thing, [=](){
|
||||
thing->setStateValue("connected", true);
|
||||
pluginStorage()->beginGroup(thing->id().toString());
|
||||
pluginStorage()->setValue("cachedIP", ip.toString());
|
||||
pluginStorage()->endGroup();
|
||||
});
|
||||
connect(client, &OwletClient::disconnected, thing, [=](){
|
||||
thing->setStateValue("connected", false);
|
||||
});
|
||||
|
||||
connect(client, &OwletClient::notificationReceived, this, [=](const QString &name, const QVariantMap ¶ms){
|
||||
qCDebug(dcOwlet()) << "***Notif" << name << params;
|
||||
if (thing->thingClassId() == digitalInputThingClassId) {
|
||||
if (params.value("id").toInt() == thing->paramValue(digitalInputThingPinParamTypeId)) {
|
||||
thing->setStateValue(digitalInputPowerStateTypeId, params.value("power").toBool());
|
||||
}
|
||||
}
|
||||
if (thing->thingClassId() == digitalOutputThingClassId) {
|
||||
if (params.value("id").toInt() == thing->paramValue(digitalOutputThingPinParamTypeId)) {
|
||||
thing->setStateValue(digitalOutputPowerStateTypeId, params.value("power").toBool());
|
||||
}
|
||||
}
|
||||
if (thing->thingClassId() == ws2812ThingClassId) {
|
||||
if (name == "GPIO.PinChanged") {
|
||||
if (params.contains("power")) {
|
||||
thing->setStateValue(ws2812PowerStateTypeId, params.value("power").toBool());
|
||||
}
|
||||
if (params.contains("brightness")) {
|
||||
thing->setStateValue(ws2812BrightnessStateTypeId, params.value("brightness").toInt());
|
||||
}
|
||||
if (params.contains("color")) {
|
||||
thing->setStateValue(ws2812ColorStateTypeId, params.value("color").value<QColor>());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client->connectToHost(ip, port);
|
||||
}
|
||||
|
||||
|
||||
void IntegrationPluginOwlet::executeAction(ThingActionInfo *info)
|
||||
{
|
||||
if (info->thing()->thingClassId() == digitalOutputThingClassId) {
|
||||
OwletClient *client = m_clients.value(info->thing());
|
||||
QVariantMap params;
|
||||
params.insert("id", info->thing()->paramValue(digitalOutputThingPinParamTypeId).toInt());
|
||||
params.insert("power", info->action().paramValue(digitalOutputPowerActionPowerParamTypeId).toBool());
|
||||
qCDebug(dcOwlet()) << "Sending ControlPin" << params;
|
||||
int id = client->sendCommand("GPIO.ControlPin", params);
|
||||
connect(client, &OwletClient::replyReceived, info, [=](int commandId, const QVariantMap ¶ms){
|
||||
if (id != commandId) {
|
||||
return;
|
||||
}
|
||||
qCDebug(dcOwlet()) << "reply from owlet:" << params;
|
||||
QString error = params.value("error").toString();
|
||||
if (error == "GPIOErrorNoError") {
|
||||
info->thing()->setStateValue(digitalOutputPowerStateTypeId, info->action().paramValue(digitalOutputPowerActionPowerParamTypeId).toBool());
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
} else {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (info->thing()->thingClassId() == ws2812ThingClassId) {
|
||||
OwletClient *client = m_clients.value(info->thing());
|
||||
QVariantMap params;
|
||||
params.insert("id", info->thing()->paramValue(ws2812ThingPinParamTypeId).toUInt());
|
||||
|
||||
if (info->action().actionTypeId() == ws2812PowerActionTypeId) {
|
||||
params.insert("power", info->action().paramValue(ws2812PowerActionPowerParamTypeId).toBool());
|
||||
}
|
||||
if (info->action().actionTypeId() == ws2812BrightnessActionTypeId) {
|
||||
params.insert("brightness", info->action().paramValue(ws2812BrightnessActionBrightnessParamTypeId).toInt());
|
||||
}
|
||||
if (info->action().actionTypeId() == ws2812ColorActionTypeId) {
|
||||
QColor color = info->action().paramValue(ws2812ColorActionColorParamTypeId).value<QColor>();
|
||||
params.insert("color", (color.rgb() & 0xFFFFFF));
|
||||
}
|
||||
|
||||
int id = client->sendCommand("GPIO.ControlPin", params);
|
||||
connect(client, &OwletClient::replyReceived, info, [=](int commandId, const QVariantMap ¶ms){
|
||||
if (id != commandId) {
|
||||
return;
|
||||
}
|
||||
qCDebug(dcOwlet()) << "reply from owlet:" << params;
|
||||
QString error = params.value("error").toString();
|
||||
if (error == "GPIOErrorNoError") {
|
||||
info->finish(Thing::ThingErrorNoError);
|
||||
} else {
|
||||
info->finish(Thing::ThingErrorHardwareFailure);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Q_ASSERT_X(false, "IntegrationPluginOwlet", "Not implemented");
|
||||
info->finish(Thing::ThingErrorUnsupportedFeature);
|
||||
}
|
||||
|
||||
void IntegrationPluginOwlet::thingRemoved(Thing *thing)
|
||||
{
|
||||
Q_UNUSED(thing)
|
||||
}
|
||||
63
owlet/integrationpluginowlet.h
Normal file
63
owlet/integrationpluginowlet.h
Normal file
@ -0,0 +1,63 @@
|
||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||
*
|
||||
* Copyright 2013 - 2021, 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 <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* 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 INTEGRATIONPLUGINOWLET_H
|
||||
#define INTEGRATIONPLUGINOWLET_H
|
||||
|
||||
#include "integrations/integrationplugin.h"
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
class ZeroConfServiceBrowser;
|
||||
class OwletClient;
|
||||
|
||||
class IntegrationPluginOwlet: public IntegrationPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginowlet.json")
|
||||
Q_INTERFACES(IntegrationPlugin)
|
||||
|
||||
public:
|
||||
explicit IntegrationPluginOwlet();
|
||||
|
||||
void init() override;
|
||||
void discoverThings(ThingDiscoveryInfo *info) override;
|
||||
void setupThing(ThingSetupInfo *info) override;
|
||||
void executeAction(ThingActionInfo *info) override;
|
||||
void thingRemoved(Thing *thing) override;
|
||||
|
||||
private:
|
||||
ZeroConfServiceBrowser *m_zeroConfBrowser = nullptr;
|
||||
|
||||
QHash<Thing*, OwletClient*> m_clients;
|
||||
|
||||
};
|
||||
|
||||
#endif // INTEGRATIONPLUGINOWLET_H
|
||||
216
owlet/integrationpluginowlet.json
Normal file
216
owlet/integrationpluginowlet.json
Normal file
@ -0,0 +1,216 @@
|
||||
{
|
||||
"displayName": "nymea owlet",
|
||||
"name": "owlet",
|
||||
"id": "699a5b6d-d90f-4554-a8de-9205768a4a98",
|
||||
"vendors": [
|
||||
{
|
||||
"displayName": "nymea GmbH",
|
||||
"name": "nymea",
|
||||
"id": "2062d64d-3232-433c-88bc-0d33c0ba2ba6",
|
||||
"thingClasses": [
|
||||
{
|
||||
"id": "5a079c4e-9309-4d98-9ff1-9beeda210958",
|
||||
"displayName": "Digital GPIO output on owlet",
|
||||
"name": "digitalOutput",
|
||||
"createMethods": ["discovery"],
|
||||
"interfaces": ["power"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "de8cda8f-b8f1-425d-ae16-fd0f5a885ca4",
|
||||
"name": "owletId",
|
||||
"displayName": "Owlet ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "31dbcdea-04f3-4a0c-b131-7eda8a92c602",
|
||||
"name": "pin",
|
||||
"displayName": "Pin number",
|
||||
"type": "uint",
|
||||
"defaultValue": 1
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "dd97a6b1-e98e-4a60-b16a-b27240b91439",
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"writable": true,
|
||||
"displayNameEvent": "Power changed",
|
||||
"displayNameAction": "Set power",
|
||||
"ioType": "digitalOutput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "673512a3-75d8-44a6-9930-198c9f1a1f03",
|
||||
"displayName": "Digital GPIO Input on owlet",
|
||||
"name": "digitalInput",
|
||||
"createMethods": ["discovery"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "dd7eca3f-13f6-4320-aaaa-b0be8fbfeebf",
|
||||
"name": "owletId",
|
||||
"displayName": "Owlet ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "f6b60a4b-e7a2-4328-884d-818b0e2a361e",
|
||||
"name": "pin",
|
||||
"displayName": "Pin number",
|
||||
"type": "uint",
|
||||
"defaultValue": 1
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "df1fbd9f-10b1-4788-a00e-de3f3f411cc6",
|
||||
"name": "power",
|
||||
"displayName": "Powered",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"displayNameEvent": "Powered changed",
|
||||
"ioType": "digitalInput"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "76f4ef8e-8e17-4528-a667-3d3f5afdd6a7",
|
||||
"name": "ws2812",
|
||||
"displayName": "WS2812 on owlet",
|
||||
"createMethods": ["discovery"],
|
||||
"interfaces": ["colorlight", "wirelessconnectable"],
|
||||
"paramTypes": [
|
||||
{
|
||||
"id": "8c00f42b-5d34-4595-8ae9-6f48056a8be0",
|
||||
"name": "owletId",
|
||||
"displayName": "Owlet ID",
|
||||
"type": "QString",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"id": "d674ee68-7f24-4dec-a75a-647a083d3580",
|
||||
"name": "pin",
|
||||
"displayName": "Pin number",
|
||||
"type": "uint",
|
||||
"defaultValue": 1
|
||||
},
|
||||
{
|
||||
"id": "6c6df8eb-cdf1-424c-b29d-60f7dd19ae41",
|
||||
"name": "ledCount",
|
||||
"displayName": "LED count",
|
||||
"type": "uint",
|
||||
"defaultValue": 1
|
||||
},
|
||||
{
|
||||
"id": "69c7f0e5-fdc4-4f9f-a117-90f165af3178",
|
||||
"name": "ledMode",
|
||||
"displayName": "LED color mode",
|
||||
"type": "QString",
|
||||
"allowedValues": [
|
||||
"RGB",
|
||||
"RBG",
|
||||
"GRB",
|
||||
"GBR",
|
||||
"BRG",
|
||||
"BGR",
|
||||
"WRGB",
|
||||
"WRBG",
|
||||
"WGRB",
|
||||
"WGBR",
|
||||
"WBRG",
|
||||
"WBRG",
|
||||
"RWGB",
|
||||
"RWBG",
|
||||
"RGWB",
|
||||
"RGBW",
|
||||
"RBWG",
|
||||
"RBGW",
|
||||
"GWRB",
|
||||
"GWBR",
|
||||
"GRWB",
|
||||
"GRBW",
|
||||
"GBWR",
|
||||
"GBRW",
|
||||
"BWRG",
|
||||
"BWGR",
|
||||
"BRWG",
|
||||
"BRGW",
|
||||
"BGWR",
|
||||
"BGRW"
|
||||
],
|
||||
"defaultValue": "RGB"
|
||||
},
|
||||
{
|
||||
"id": "c4d99f98-1b46-4b38-bdd4-4bd5559dbb6f",
|
||||
"name": "ledClock",
|
||||
"displayName": "LED clock speed",
|
||||
"type": "QString",
|
||||
"allowedValues": ["400kHz", "800kHz"],
|
||||
"defaultValue": "800kHz"
|
||||
}
|
||||
],
|
||||
"stateTypes": [
|
||||
{
|
||||
"id": "0dbdd49b-578d-4404-87d2-b5a921df6aa6",
|
||||
"name": "connected",
|
||||
"displayName": "Connected",
|
||||
"displayNameEvent": "Connected or disconnected",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"cached": false
|
||||
},
|
||||
{
|
||||
"id": "58a8b3ca-720c-458e-b045-b99b5aadabd7",
|
||||
"name": "power",
|
||||
"displayName": "Power",
|
||||
"displayNameEvent": "Power changed",
|
||||
"displayNameAction": "Set power",
|
||||
"type": "bool",
|
||||
"defaultValue": false,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "b56a9368-db2a-4ee6-99de-9ee8e1ffebd3",
|
||||
"name": "brightness",
|
||||
"displayName": "Brightness",
|
||||
"displayNameEvent": "Brightness changed",
|
||||
"displayNameAction": "Set brightness",
|
||||
"type": "int",
|
||||
"minValue": 0,
|
||||
"maxValue": 100,
|
||||
"unit": "Percentage",
|
||||
"defaultValue": 100,
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "684c9118-20f3-41a0-928e-b7290d40166d",
|
||||
"name": "color",
|
||||
"displayName": "Color",
|
||||
"displayNameEvent": "Color changed",
|
||||
"displayNameAction": "Set color",
|
||||
"type": "QColor",
|
||||
"defaultValue": "white",
|
||||
"writable": true
|
||||
},
|
||||
{
|
||||
"id": "f92ea731-a86e-49b5-955b-9c245c7f874f",
|
||||
"name": "colorTemperature",
|
||||
"displayName": "Color temperature",
|
||||
"displayNameEvent": "Color temperature changed",
|
||||
"displayNameAction": "Set color temperature",
|
||||
"type": "int",
|
||||
"minValue": 0,
|
||||
"maxValue": 100,
|
||||
"defaultValue": 50,
|
||||
"writable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
owlet/owlet.pro
Normal file
13
owlet/owlet.pro
Normal file
@ -0,0 +1,13 @@
|
||||
include(../plugins.pri)
|
||||
|
||||
QT += network
|
||||
|
||||
SOURCES += \
|
||||
integrationpluginowlet.cpp \
|
||||
owletclient.cpp
|
||||
|
||||
HEADERS += \
|
||||
integrationpluginowlet.h \
|
||||
owletclient.h
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user