Christian Fetzer 2022-10-02 15:52:02 +02:00
parent 68e9cd8c01
commit effe59e9b9
10 changed files with 1206 additions and 0 deletions

9
debian/control vendored
View File

@ -210,6 +210,15 @@ Description: nymea integration plugin for elgato
This package contains the nymea integration plugin for devices from Elgato
Package: nymea-plugin-espuino
Architecture: any
Depends: ${shlibs:Depends},
${misc:Depends},
Conflicts: nymea-plugins-translations (< 1.0.1)
Description: nymea integration plugin for ESPuino
This package contains the nymea integration plugin for ESPuino devices.
Package: nymea-plugin-fastcom
Architecture: any
Depends: ${misc:Depends},

View File

@ -0,0 +1,2 @@
usr/lib/@DEB_HOST_MULTIARCH@/nymea/plugins/libnymea_integrationpluginespuino.so
espuino/translations/*qm usr/share/nymea/translations/

29
espuino/README.md Normal file
View File

@ -0,0 +1,29 @@
# ESPuino
This plugin allows to integrate nymea with [ESPunio](https://github.com/biologist79/ESPuino),
a software for Rfid-controlled music players running on ESP32 hardware.
ESPuino boxes can't be bought off the shelf, but there's a (mostly
German speaking) community in the [ESPuino forum](https://forum.espuino.de/)
that provides a lot of documentation and ideas for building your own custom
ESPuino.
## Usage
As a prerequisite, ESPuino has to be compiled with WiFi and MQTT
enabled and the ESPuino must have been set up to connect to your home
WiFi. For the mDNS based auto discovery to work, the hostname must start with
`espuino`.
Then the ESPuino can be added to nymea.
## Supported features
- Display current title and cover art
- Control volume
- Play/pause
- Lock hardware controls
- Browse SD-Card and start playback
- Control LED brightness
- Configure sleep or repeat modes
- Show battery status

12
espuino/espuino.pro Normal file
View File

@ -0,0 +1,12 @@
include(../plugins.pri)
QT += network \
websockets
PKGCONFIG += nymea-mqtt
SOURCES += \
integrationpluginespuino.cpp
HEADERS += \
integrationpluginespuino.h

View File

@ -0,0 +1,556 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <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 "integrationpluginespuino.h"
#include "integrations/integrationplugin.h"
#include "plugininfo.h"
#include "network/networkaccessmanager.h"
#include "network/mqtt/mqttprovider.h"
#include "network/mqtt/mqttchannel.h"
#include "network/zeroconf/zeroconfservicebrowser.h"
#include "platform/platformzeroconfcontroller.h"
#include <mqttclient.h>
#include <QWebSocket>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QUrlQuery>
void IntegrationPluginEspuino::init()
{
m_zeroConfBrowser = hardwareManager()->zeroConfController()->createServiceBrowser("_http._tcp");
}
void IntegrationPluginEspuino::discoverThings(ThingDiscoveryInfo *info)
{
foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) {
QRegExp match("espuino.*");
if (!match.exactMatch(entry.name())) {
continue;
}
qCDebug(dcESPuino()) << "Found device:" << entry;
ThingDescriptor descriptor(info->thingClassId(), entry.hostName(), entry.hostAddress().toString());
ParamList params;
params << Param(espuinoThingHostnameParamTypeId, entry.hostName());
descriptor.setParams(params);
Things existingThings = myThings().filterByParam(espuinoThingHostnameParamTypeId, entry.hostName());
if (existingThings.count() == 1) {
qCDebug(dcESPuino()) << "This device already exists in the system!";
descriptor.setThingId(existingThings.first()->id());
}
info->addThingDescriptor(descriptor);
}
info->finish(Thing::ThingErrorNoError);
}
void IntegrationPluginEspuino::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
MqttChannel *channel;
if (myThings().findByParams(ParamList() << Param(espuinoThingHostnameParamTypeId, thing->paramValue(espuinoThingHostnameParamTypeId).toString())) == nullptr){
qCDebug(dcESPuino) << "Creating MQTT channel for new device.";
channel = hardwareManager()->mqttProvider()->createChannel(QHostAddress(getHost(thing)), {"Cmnd/ESPuino", "State/ESPuino"});
if (!channel) {
qCWarning(dcESPuino) << "Failed to create MQTT channel.";
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error creating MQTT channel. Please check MQTT server settings."));
return;
}
qCInfo(dcESPuino) << "Reconfiguring MQTT settings via Websocket.";
QWebSocket *ws = new QWebSocket(QString(), QWebSocketProtocol::VersionLatest, info);
connect(ws, &QWebSocket::connected, info, [channel, ws](){
QJsonDocument jsonRequest{QJsonObject
{
{"mqtt", QJsonObject{{"mqttEnable", "1"},
{"mqttClientId", channel->clientId()},
{"mqttServer", channel->serverAddress().toString()},
{"mqttUser", channel->username()},
{"mqttPwd", channel->password()},
{"mqttPort", QString::number(channel->serverPort())}}}
}};
ws->sendTextMessage(jsonRequest.toJson(QJsonDocument::Compact));
});
connect(ws, &QWebSocket::textMessageReceived, info, [this, info, thing, channel, ws](const QString &message){
ws->close();
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(message.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
qCWarning(dcESPuino()) << "Json parse error:" << parseError.error << parseError.errorString() << "Received:" << message;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Failed to configure MQTT via Websocket."));
hardwareManager()->mqttProvider()->releaseChannel(channel);
return;
}
if (jsonDoc.object().value("status").toString() != "ok") {
qCWarning(dcESPuino()) << "Failed to configure MQTT via websocket. Received:" << message;
info->finish(Thing::ThingErrorSetupFailed, QT_TR_NOOP("Failed to configure MQTT via Websocket."));
hardwareManager()->mqttProvider()->releaseChannel(channel);
return;
}
pluginStorage()->beginGroup(thing->id().toString());
pluginStorage()->setValue("clientId", channel->clientId());
pluginStorage()->setValue("username", channel->username());
pluginStorage()->setValue("password", channel->password());
pluginStorage()->endGroup();
qCInfo(dcESPuino) << "Restarting box to apply new MQTT config.";
QUrl url(QString("http://%1/restart").arg(getHost(thing)));
QNetworkRequest request(url);
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
info->finish(Thing::ThingErrorNoError);
});
QUrl url(QString("ws://%1/ws").arg(getHost(thing)));
ws->open(url);
} else {
qCDebug(dcESPuino) << "Creating MQTT channel for existing device.";
pluginStorage()->beginGroup(thing->id().toString());
QString clientId = pluginStorage()->value("clientId").toString();
QString username = pluginStorage()->value("username").toString();
QString password = pluginStorage()->value("password").toString();
pluginStorage()->endGroup();
channel = hardwareManager()->mqttProvider()->createChannel(clientId, username, password, QHostAddress(getHost(thing)), {"Cmnd/ESPuino", "State/ESPuino"});
if (!channel) {
qCWarning(dcESPuino) << "Failed to create MQTT channel.";
info->finish(Thing::ThingErrorHardwareFailure, QT_TR_NOOP("Error creating MQTT channel. Please check MQTT server settings."));
return;
}
info->finish(Thing::ThingErrorNoError);
}
m_mqttChannels.insert(thing, channel);
connect(channel, &MqttChannel::clientConnected, this, &IntegrationPluginEspuino::onClientConnected);
connect(channel, &MqttChannel::clientDisconnected, this, &IntegrationPluginEspuino::onClientDisconnected);
connect(channel, &MqttChannel::publishReceived, this, &IntegrationPluginEspuino::onPublishReceived);
}
void IntegrationPluginEspuino::thingRemoved(Thing *thing)
{
qCDebug(dcESPuino) << "Device removed" << thing->name();
if (m_mqttChannels.contains(thing)) {
qCDebug(dcESPuino) << "Releasing MQTT channel";
MqttChannel* channel = m_mqttChannels.take(thing);
hardwareManager()->mqttProvider()->releaseChannel(channel);
}
}
void IntegrationPluginEspuino::onClientConnected(MqttChannel *channel)
{
Thing *thing = m_mqttChannels.key(channel);
qCDebug(dcESPuino) << "Thing connected" << thing->name();
if (!thing) {
qCWarning(dcESPuino) << "Received a MQTT connected message from a client but don't have a matching thing";
return;
}
thing->setStateValue(espuinoConnectedStateTypeId, true);
}
void IntegrationPluginEspuino::onClientDisconnected(MqttChannel *channel)
{
Thing *thing = m_mqttChannels.key(channel);
qCDebug(dcESPuino) << "Thing disconnected" << thing->name();
if (!thing) {
qCWarning(dcESPuino) << "Received a MQTT disconnected message from a client but don't have a matching thing";
return;
}
thing->setStateValue(espuinoConnectedStateTypeId, false);
}
void IntegrationPluginEspuino::onPublishReceived(MqttChannel *channel, const QString &topic, const QByteArray &payload)
{
qCDebug(dcESPuino) << "Publish received" << topic << payload;
Thing *thing = m_mqttChannels.key(channel);
if (!thing) {
qCWarning(dcESPuino) << "Received a publish message from a client but don't have a matching thing";
return;
}
if (topic == "State/ESPuino/State") {
thing->setStateValue(espuinoConnectedStateTypeId, payload == "Online");
} else if (topic == "State/ESPuino/Playmode") {
thing->setStateValue(espuinoPlaybackStatusStateTypeId, payload == "0" ? "Stopped" : "Playing");
QString playmode = "None";
if (payload == "0") {
playmode = "None";
} else if (payload == "1") {
playmode = "Single track";
} else if (payload == "2") {
playmode = "Single track (loop)";
} else if (payload == "12") {
playmode = "Single track of a directory (random). Followed by sleep.";
} else if (payload == "3") {
playmode = "Audiobook";
} else if (payload == "4") {
playmode = "Audiobook (loop)";
} else if (payload == "5") {
playmode = "All tracks of a directory (sorted alph.)";
} else if (payload == "6") {
playmode = "All tracks of a directory (random)";
} else if (payload == "7") {
playmode = "All tracks of a directory (sorted alph., loop)";
} else if (payload == "9") {
playmode = "All tracks of a directory (random, loop)";
} else if (payload == "8") {
playmode = "Webradio";
} else if (payload == "11") {
playmode = "List (files from SD and/or webstreams) from local .m3u-File";
} else if (payload == "10") {
playmode = "Busy";
} else {
qCWarning(dcESPuino) << "Unknown playmode received" << payload;
}
thing->setStateValue(espuinoPlaymodeStateTypeId, playmode);
} else if (topic == "State/ESPuino/Loudness") {
bool ok;
int volume = payload.toInt(&ok);
if (ok) {
thing->setStateValue(espuinoVolumeStateTypeId, volume);
} else {
qCWarning(dcESPuino) << "Failed to read numeric volume value" << payload;
thing->setStateValue(espuinoVolumeStateTypeId, 0);
}
} else if (topic == "State/ESPuino/Track") {
thing->setStateValue(espuinoTitleStateTypeId, payload);
} else if (topic == "State/ESPuino/CoverChanged") {
thing->setStateValue(espuinoArtworkStateTypeId, QString("http://%1/cover?%2").arg(getHost(thing)).arg(QDateTime::currentMSecsSinceEpoch()));
} else if (topic == "State/ESPuino/LedBrightness") {
bool ok;
int brightness = payload.toInt(&ok);
if (ok) {
thing->setStateValue(espuinoBrightnessStateTypeId, brightness);
} else {
qCWarning(dcESPuino) << "Failed to read numeric brightness value" << payload;
thing->setStateValue(espuinoBrightnessStateTypeId, 0);
}
} else if (topic == "State/ESPuino/RepeatMode") {
if (payload == "3") {
thing->setStateValue(espuinoRepeatStateTypeId, "All");
} if (payload == "1") {
thing->setStateValue(espuinoRepeatStateTypeId, "One");
} else {
thing->setStateValue(espuinoRepeatStateTypeId, "None");
}
} else if (topic == "State/ESPuino/WifiRssi") {
bool ok;
int rssi = payload.toInt(&ok);
if (ok) {
thing->setStateValue(espuinoSignalStrengthStateTypeId, qMin(100, qMax(0, (rssi + 100) * 2)));
} else {
thing->setStateValue(espuinoSignalStrengthStateTypeId, 0);
}
} else if (topic == "State/ESPuino/LockControl") {
thing->setStateValue(espuinoChildLockStateTypeId, payload == "ON");
} else if (topic == "State/ESPuino/SleepTimer") {
if (payload == "EOP") {
thing->setStateValue(espuinoSleepmodeStateTypeId, "End of playlist");
} else if (payload == "EOT") {
thing->setStateValue(espuinoSleepmodeStateTypeId, "End of track");
} else if (payload == "EO5T") {
thing->setStateValue(espuinoSleepmodeStateTypeId, "End of five tracks");
} else if (payload == "0") {
thing->setStateValue(espuinoSleepmodeStateTypeId, "None");
} else {
bool ok;
int timer = payload.toInt(&ok);
if (ok) {
thing->setStateValue(espuinoSleepmodeStateTypeId, "Timer");
thing->setStateValue(espuinoSleeptimerStateTypeId, timer);
} else {
qCWarning(dcESPuino) << "Failed to read numeric sleep timer value" << payload;
thing->setStateValue(espuinoSleepmodeStateTypeId, "None");
}
}
} else if (topic == "State/ESPuino/Battery") {
bool ok;
float battery = payload.toFloat(&ok);
if (ok) {
thing->setStateValue(espuinoBatteryLevelStateTypeId, battery);
thing->setStateValue(espuinoBatteryCriticalStateTypeId, battery < 5.0f);
} else {
qCWarning(dcESPuino) << "Failed to read numeric battery level value" << payload;
thing->setStateValue(espuinoBatteryLevelStateTypeId, 0);
thing->setStateValue(espuinoBatteryCriticalStateTypeId, false);
}
}
// Finish pending action.
QPointer<ThingActionInfo> thingActionInfo = m_pendingActions.take(topic);
if (!thingActionInfo.isNull()) {
thingActionInfo->finish(Thing::ThingErrorNoError);
}
}
void IntegrationPluginEspuino::executeAction(ThingActionInfo *info)
{
Thing *thing = info->thing();
Action action = info->action();
MqttChannel *channel = m_mqttChannels.value(thing);
if (!channel) {
qCWarning(dcESPuino) << "No valid MQTT channel for thing" << thing->name();
return info->finish(Thing::ThingErrorThingNotFound);
}
// See: https://github.com/biologist79/ESPuino#mqtt-topics-and-their-ranges
QString topic;
QByteArray payload;
if (action.actionTypeId() == espuinoVolumeActionTypeId) {
topic = "Cmnd/ESPuino/Loudness";
payload = QByteArray::number(action.param(espuinoVolumeActionVolumeParamTypeId).value().toInt());
m_pendingActions.insert("State/ESPuino/Loudness", info);
} else if (action.actionTypeId() == espuinoIncreaseVolumeActionTypeId) {
topic = "Cmnd/ESPuino/Loudness";
payload = QByteArray::number(thing->stateValue(espuinoVolumeStateTypeId).toInt() + 1);
m_pendingActions.insert("State/ESPuino/Loudness", info);
} else if (action.actionTypeId() == espuinoDecreaseVolumeActionTypeId) {
topic = "Cmnd/ESPuino/Loudness";
payload = QByteArray::number(thing->stateValue(espuinoVolumeStateTypeId).toInt() - 1);
m_pendingActions.insert("State/ESPuino/Loudness", info);
} else if (action.actionTypeId() == espuinoStopActionTypeId) {
topic = "Cmnd/ESPuino/TrackControl";
payload = "1";
m_pendingActions.insert("State/ESPuino/TrackControl", info);
} else if (action.actionTypeId() == espuinoSkipNextActionTypeId) {
topic = "Cmnd/ESPuino/TrackControl";
payload = "4";
m_pendingActions.insert("State/ESPuino/TrackControl", info);
} else if (action.actionTypeId() == espuinoSkipBackActionTypeId) {
topic = "Cmnd/ESPuino/TrackControl";
payload = "5";
m_pendingActions.insert("State/ESPuino/TrackControl", info);
} else if (action.actionTypeId() == espuinoPlayActionTypeId) {
topic = "Cmnd/ESPuino/TrackControl";
payload = "3";
m_pendingActions.insert("State/ESPuino/TrackControl", info);
} else if (action.actionTypeId() == espuinoPauseActionTypeId) {
topic = "Cmnd/ESPuino/TrackControl";
payload = "3";
m_pendingActions.insert("State/ESPuino/TrackControl", info);
} else if (action.actionTypeId() == espuinoBrightnessActionTypeId) {
topic = "Cmnd/ESPuino/LedBrightness";
payload = QByteArray::number(action.param(espuinoBrightnessActionBrightnessParamTypeId).value().toInt());
m_pendingActions.insert("State/ESPuino/LedBrightness", info);
} else if (action.actionTypeId() == espuinoRepeatActionTypeId) {
topic = "Cmnd/ESPuino/RepeatMode";
QString repeat = action.param(espuinoRepeatActionRepeatParamTypeId).value().toString();
if (repeat == "One") {
payload = "1";
} else if (repeat == "All") {
payload = "3";
} else {
payload = "0";
}
m_pendingActions.insert("State/ESPuino/RepeatMode", info);
} else if (action.actionTypeId() == espuinoChildLockActionTypeId) {
topic = "Cmnd/ESPuino/LockControls";
payload = action.param(espuinoChildLockActionChildLockParamTypeId).value().toBool() ? "ON" : "OFF";
m_pendingActions.insert("State/ESPuino/LockControls", info);
} else if (action.actionTypeId() == espuinoSleepmodeActionTypeId) {
topic = "Cmnd/ESPuino/SleepTimer";
QString sleepmode = action.param(espuinoSleepmodeActionSleepmodeParamTypeId).value().toString();
if (sleepmode == "None") {
payload = "0";
} else if (sleepmode == "End of playlist") {
payload = "EOP";
} else if (sleepmode == "End of track") {
payload = "EOT";
} else if (sleepmode == "End of five tracks") {
payload = "EO5T";
} else {
payload = QByteArray::number(thing->stateValue(espuinoSleeptimerStateTypeId).toInt());
}
m_pendingActions.insert("State/ESPuino/SleepTimer", info);
} else if (action.actionTypeId() == espuinoSleeptimerActionTypeId) {
thing->setStateValue(espuinoSleeptimerStateTypeId, action.param(espuinoSleeptimerActionSleeptimerParamTypeId).value().toUInt());
info->finish(Thing::ThingErrorNoError);
}
if (!topic.isEmpty()) {
qCDebug(dcESPuino) << "Publishing:" << topic << payload;
channel->publish(topic, payload);
}
return;
}
void IntegrationPluginEspuino::browseThing(BrowseResult *result)
{
QUrlQuery id(result->itemId());
browseThing(result, id.queryItemValue("path"));
}
void IntegrationPluginEspuino::browseThing(BrowseResult *result, const QString &path)
{
QUrl url(QString("http://%1/explorer?path=%2").arg(getHost(result->thing())).arg(path.isEmpty() ? "/" : path));
QNetworkRequest request(url);
QNetworkReply *reply = hardwareManager()->networkManager()->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, result, [result, reply, path, this]() {
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcESPuino()) << "Error fetching paths";
result->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
QByteArray data = reply->readAll();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(dcESPuino()) << "Error parsing json" << data;
result->finish(Thing::ThingErrorHardwareFailure);
return;
}
//qCDebug(dcESPuino()) << "Reply:" << qUtf8Printable(jsonDoc.toJson());
QVariantList variantList = jsonDoc.toVariant().toList();
foreach (const QVariant &element, variantList) {
QVariantMap variantMap = element.toMap();
QUrlQuery id;
id.addQueryItem("name", variantMap.value("name").toString());
id.addQueryItem("path", path + "/" + variantMap.value("name").toString());
if (variantMap.value("dir").toBool()) {
id.addQueryItem("playmode", QString::number(5));
id.addQueryItem("type", "dir");
} else if (variantMap.value("name").toString().contains(QRegExp("\\.(:?mp3|ogg|wav|wma|acc|m4a|flac)$", Qt::CaseInsensitive))) {
id.addQueryItem("playmode", QString::number(1));
id.addQueryItem("type", "audiofile");
} else if (variantMap.value("name").toString().endsWith(".m3u", Qt::CaseInsensitive)) {
id.addQueryItem("playmode", QString::number(11));
id.addQueryItem("type", "playlist");
}
result->addItem(browserItemFromQuery(id));
}
result->finish(Thing::ThingErrorNoError);
});
}
void IntegrationPluginEspuino::browserItem(BrowserItemResult *result)
{
QUrlQuery id(result->itemId());
result->finish(browserItemFromQuery(id));
}
BrowserItem IntegrationPluginEspuino::browserItemFromQuery(const QUrlQuery &id)
{
BrowserItem item;
item.setDisplayName(id.queryItemValue("name"));
if (id.queryItemValue("type") == "dir") {
item.setId(id.toString());
item.setIcon(BrowserItem::BrowserIconFolder);
item.setBrowsable(true);
item.setActionTypeIds({espuinoPlayAllBrowserItemActionTypeId});
} else if (id.queryItemValue("type") == "audiofile") {
item.setId(id.toString());
item.setIcon(BrowserItem::BrowserIconMusic);
item.setExecutable(true);
} else if (id.queryItemValue("type") == "playlist") {
item.setId(id.toString());
item.setIcon(BrowserItem::BrowserIconDocument);
item.setExecutable(true);
} else {
item.setId(id.toString());
item.setIcon(BrowserItem::BrowserIconFile);
}
return item;
}
void IntegrationPluginEspuino::IntegrationPluginEspuino::executeBrowserItem(BrowserActionInfo *info)
{
Thing *thing = info->thing();
BrowserAction action = info->browserAction();
QUrl url(QString("http://%1/exploreraudio?%2").arg(getHost(thing)).arg(action.itemId()));
qCInfo(dcESPuino) << "Starting playback" << url.toString();
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply]() {
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcESPuino()) << "Fail to execute play action";
}
});
}
void IntegrationPluginEspuino::executeBrowserItemAction(BrowserItemActionInfo *info)
{
Thing *thing = info->thing();
BrowserItemAction action = info->browserItemAction();
if (action.actionTypeId() == espuinoPlayAllBrowserItemActionTypeId) {
QUrl url(QString("http://%1/exploreraudio?%2").arg(getHost(thing)).arg(action.itemId()));
qCInfo(dcESPuino) << "Starting playback" << url.toString();
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = hardwareManager()->networkManager()->post(request, QByteArray());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [reply]() {
if (reply->error() != QNetworkReply::NoError) {
qCWarning(dcESPuino()) << "Fail to execute play action";
}
});
}
}
QString IntegrationPluginEspuino::getHost(Thing *thing) const
{
QString hostName = thing->paramValue(espuinoThingHostnameParamTypeId).toString();
ZeroConfServiceEntry zeroConfEntry;
foreach (const ZeroConfServiceEntry &entry, m_zeroConfBrowser->serviceEntries()) {
if (hostName == entry.hostName()) {
zeroConfEntry = entry;
}
}
QString host;
pluginStorage()->beginGroup(thing->id().toString());
if (zeroConfEntry.isValid()) {
host = zeroConfEntry.hostAddress().toString();
pluginStorage()->setValue("cachedAddress", host);
} else if (pluginStorage()->contains("cachedAddress")){
host = pluginStorage()->value("cachedAddress").toString();
} else {
qCWarning(dcESPuino()) << "Unable to determine IP address for:" << hostName;
}
pluginStorage()->endGroup();
return host;
}

View File

@ -0,0 +1,78 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* 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 <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 INTEGRATIONPLUGINESPUINO_H
#define INTEGRATIONPLUGINESPUINO_H
#include "integrations/integrationplugin.h"
#include "plugintimer.h"
#include <QHash>
#include "extern-plugininfo.h"
class MqttChannel;
class ZeroConfServiceBrowser;
class IntegrationPluginEspuino : public IntegrationPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginespuino.json")
Q_INTERFACES(IntegrationPlugin)
public:
void init() override;
void discoverThings(ThingDiscoveryInfo *info) override;
void setupThing(ThingSetupInfo *info) override;
void thingRemoved(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
void browseThing(BrowseResult *result) override;
void browseThing(BrowseResult *result, const QString &path);
void browserItem(BrowserItemResult *result) override;
BrowserItem browserItemFromQuery(const QUrlQuery &query);
void executeBrowserItem(BrowserActionInfo *info) override;
void executeBrowserItemAction(BrowserItemActionInfo *info) override;
private slots:
void onClientConnected(MqttChannel *channel);
void onClientDisconnected(MqttChannel *channel);
void onPublishReceived(MqttChannel* channel, const QString &topic, const QByteArray &payload);
private:
QString getHost(Thing *thing) const;
ZeroConfServiceBrowser *m_zeroConfBrowser;
QHash<Thing*, MqttChannel*> m_mqttChannels;
QHash<ThingClassId, ParamTypeId> m_ipAddressParamTypeMap;
QMap<QString, QPointer<ThingActionInfo>> m_pendingActions;
};
#endif // INTEGRATIONPLUGINESPUINO_H

View File

@ -0,0 +1,261 @@
{
"name": "ESPuino",
"displayName": "ESPuino",
"id": "5f8ba72b-d3fb-4efe-952d-a927bed20cfe",
"vendors": [
{
"name": "ESPuino",
"displayName": "ESPuino",
"id": "58c8eb30-98a4-44fd-aaac-cb2a7aae7e8a",
"thingClasses": [
{
"id": "ee24ce2b-d34a-4c2c-85f3-9d895d17f414",
"name": "espuino",
"displayName": "ESPuino",
"createMethods": ["discovery"],
"interfaces": ["mediaplayer", "mediametadataprovider", "volumecontroller", "wirelessconnectable", "battery", "childlock"],
"browsable": true,
"paramTypes": [
{
"id": "2a9c9427-3e4e-4473-805e-c25242cfc621",
"name": "hostname",
"displayName": "Hostname",
"type": "QString",
"readOnly": true
}
],
"stateTypes": [
{
"id": "edbff474-0cdc-488c-a9e9-970b25ce7548",
"name": "connected",
"displayName": "Connected",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "57bd1bab-d872-4315-b53e-1157fe3889d4",
"name": "signalStrength",
"displayName": "Signal strength",
"type": "uint",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"cached": false
},
{
"id": "bee497e6-a320-458a-9006-ddfe4c7c37c2",
"name": "batteryCritical",
"displayName": "Battery critical",
"type": "bool",
"defaultValue": false,
"cached": false
},
{
"id": "9fd8f882-8240-492f-8c6b-b5477e26623e",
"name": "batteryLevel",
"displayName": "Battery level",
"type": "int",
"unit": "Percentage",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0,
"cached": false
},
{
"id": "dd1cfb1f-fec4-4035-9c02-562a6fba683d",
"name": "playbackStatus",
"displayName": "Playback status",
"type": "QString",
"possibleValues": ["Playing", "Paused", "Stopped"],
"defaultValue": "Stopped",
"writable": false,
"cached": false
},
{
"id": "a274e048-9820-444a-b5de-a3a421c855a2",
"name": "title",
"displayName": "Title",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "5acce950-cdac-44ea-963d-0635afcabdca",
"name": "playmode",
"displayName": "Playmode",
"type": "QString",
"possibleValues": [
"None",
"Single track",
"Single track (loop)",
"Single track of a directory (random). Followed by sleep.",
"Audiobook",
"Audiobook (loop)",
"All tracks of a directory (sorted alph.)",
"All tracks of a directory (random)",
"All tracks of a directory (sorted alph., loop)",
"All tracks of a directory (random, loop)",
"Webradio",
"List (files from SD and/or webstreams) from local .m3u-File",
"Busy"],
"defaultValue": "None",
"writable": false,
"cached": false
},
{
"id": "27b5ff3b-bd60-411f-b8e3-b1c8f6897bec",
"name": "repeat",
"displayName": "Repeat mode",
"type": "QString",
"possibleValues": [
"None",
"One",
"All"
],
"displayNameAction": "Set repeat",
"defaultValue": "None",
"writable": true,
"cached": false
},
{
"id": "93a5098a-a41a-46ee-8613-266d4f9ed69a",
"displayName": "Volume",
"name": "volume",
"type": "int",
"minValue": "0",
"maxValue": "21",
"displayNameAction": "Set volume",
"defaultValue": 0,
"writable": true,
"cached": false
},
{
"id": "595908c1-57b1-4303-a0ca-4c64f3cb1907",
"name": "brightness",
"displayName": "LED brightness",
"type": "int",
"minValue": 0,
"maxValue": 255,
"displayNameAction": "Set LED brightness",
"writable": true,
"defaultValue": 0,
"ioType": "analogOutput",
"cached": false
},
{
"id": "03e7a5e2-9434-47e8-91ad-03610601b925",
"name": "childLock",
"displayName": "Locl controls",
"type": "bool",
"displayNameAction": "Enable/disable control lock",
"writable": true,
"defaultValue": false,
"cached": false
},
{
"id": "19bd1456-2e4f-444a-a586-75bf6cc9fb73",
"name": "sleepmode",
"displayName": "Sleepmode",
"type": "QString",
"possibleValues": ["None", "End of playlist", "End of track", "End of five tracks", "Timer"],
"defaultValue": "None",
"displayNameAction": "Set Sleepmode",
"writable": true,
"cached": false
},
{
"id": "4c7594e4-70e7-4f0c-aae4-02e3993ffa1d",
"name": "sleeptimer",
"displayName": "Sleeptimer",
"type": "uint",
"unit": "Minutes",
"defaultValue": 10,
"displayNameAction": "Set Sleeptimer",
"writable": true,
"cached": false
},
{
"id": "f84ccfc3-0698-40ff-b413-53f0064ce663",
"name": "artwork",
"displayName": "Artwork",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "67a5b71e-ec88-4272-8d68-9562b7f786cf",
"name": "artist",
"displayName": "Artist",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "c1af97a6-f061-4082-8bf5-595728b03ab1",
"name": "collection",
"displayName": "Collection",
"type": "QString",
"defaultValue": "",
"cached": false
},
{
"id": "c7814ee8-52b1-4cc9-b8f4-f3f91ad8f33e",
"displayName": "Player Type",
"name": "playerType",
"type": "QString",
"possibleValues": ["audio", "video"],
"defaultValue": "audio",
"cached": false
}
],
"actionTypes": [
{
"id": "d045e491-c83b-4155-85ef-abc28a391402",
"name": "increaseVolume",
"displayName": "Increase volume"
},
{
"id": "16ae2d6a-68cc-497f-9e5d-2fa1f5f7107a",
"name": "decreaseVolume",
"displayName": "Decrease volume"
},
{
"id": "e04b74cc-cf74-482c-908d-8df294bd5ec8",
"name": "skipBack",
"displayName": "Prev"
},
{
"id": "d46f0b61-d406-4302-adc3-6bbc00fc2a8f",
"name": "stop",
"displayName": "Stop"
},
{
"id": "4e3b2f50-82dc-4f51-a9e5-69012985b491",
"name": "play",
"displayName": "Play"
},
{
"id": "b7128827-b429-4583-bc34-1ef4e7987809",
"name": "pause",
"displayName": "Pause"
},
{
"id": "25301c30-727c-43fd-bf3b-f7b3916947c7",
"name": "skipNext",
"displayName": "Next"
}
],
"browserItemActionTypes": [
{
"id": "ccb210ac-5819-4614-897b-e5a0b130a38a",
"name": "playAll",
"displayName": "Play All"
}
]
}
]
}
]
}

14
espuino/meta.json Normal file
View File

@ -0,0 +1,14 @@
{
"title": "ESPunio",
"tagline": "Remote control ESPunio (Rfid-controller musicplayer) via MQTT.",
"stability": "community",
"offline": true,
"technologies": [
"mqtt",
"network"
],
"categories": [
"dyi",
"multimedia",
]
}

View File

@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
<context>
<name>ESPuino</name>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="66"/>
<source>Artist</source>
<extracomment>The name of the StateType ({67a5b71e-ec88-4272-8d68-9562b7f786cf}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="69"/>
<source>Artwork</source>
<extracomment>The name of the StateType ({f84ccfc3-0698-40ff-b413-53f0064ce663}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="72"/>
<source>Battery critical</source>
<extracomment>The name of the StateType ({bee497e6-a320-458a-9006-ddfe4c7c37c2}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="75"/>
<source>Battery level</source>
<extracomment>The name of the StateType ({9fd8f882-8240-492f-8c6b-b5477e26623e}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="78"/>
<source>Collection</source>
<extracomment>The name of the StateType ({c1af97a6-f061-4082-8bf5-595728b03ab1}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="81"/>
<source>Connected</source>
<extracomment>The name of the StateType ({edbff474-0cdc-488c-a9e9-970b25ce7548}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="84"/>
<source>Decrease volume</source>
<extracomment>The name of the ActionType ({16ae2d6a-68cc-497f-9e5d-2fa1f5f7107a}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="87"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="90"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="93"/>
<source>ESPuino</source>
<extracomment>The name of the ThingClass ({ee24ce2b-d34a-4c2c-85f3-9d895d17f414})
----------
The name of the vendor ({58c8eb30-98a4-44fd-aaac-cb2a7aae7e8a})
----------
The name of the plugin ESPuino ({5f8ba72b-d3fb-4efe-952d-a927bed20cfe})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="96"/>
<source>Enable/disable control lock</source>
<extracomment>The name of the ActionType ({03e7a5e2-9434-47e8-91ad-03610601b925}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="99"/>
<source>Hostname</source>
<extracomment>The name of the ParamType (ThingClass: espuino, Type: thing, ID: {2a9c9427-3e4e-4473-805e-c25242cfc621})</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="102"/>
<source>Increase volume</source>
<extracomment>The name of the ActionType ({d045e491-c83b-4155-85ef-abc28a391402}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="105"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="108"/>
<source>LED brightness</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: brightness, ID: {595908c1-57b1-4303-a0ca-4c64f3cb1907})
----------
The name of the StateType ({595908c1-57b1-4303-a0ca-4c64f3cb1907}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="111"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="114"/>
<source>Locl controls</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: childLock, ID: {03e7a5e2-9434-47e8-91ad-03610601b925})
----------
The name of the StateType ({03e7a5e2-9434-47e8-91ad-03610601b925}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="117"/>
<source>Next</source>
<extracomment>The name of the ActionType ({25301c30-727c-43fd-bf3b-f7b3916947c7}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="120"/>
<source>Pause</source>
<extracomment>The name of the ActionType ({b7128827-b429-4583-bc34-1ef4e7987809}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="123"/>
<source>Play</source>
<extracomment>The name of the ActionType ({4e3b2f50-82dc-4f51-a9e5-69012985b491}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="126"/>
<source>Play All</source>
<extracomment>The name of the Browser Item ActionType ({ccb210ac-5819-4614-897b-e5a0b130a38a}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="129"/>
<source>Playback status</source>
<extracomment>The name of the StateType ({dd1cfb1f-fec4-4035-9c02-562a6fba683d}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="132"/>
<source>Player Type</source>
<extracomment>The name of the StateType ({c7814ee8-52b1-4cc9-b8f4-f3f91ad8f33e}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="135"/>
<source>Playmode</source>
<extracomment>The name of the StateType ({5acce950-cdac-44ea-963d-0635afcabdca}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="138"/>
<source>Prev</source>
<extracomment>The name of the ActionType ({e04b74cc-cf74-482c-908d-8df294bd5ec8}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="141"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="144"/>
<source>Repeat mode</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: repeat, ID: {27b5ff3b-bd60-411f-b8e3-b1c8f6897bec})
----------
The name of the StateType ({27b5ff3b-bd60-411f-b8e3-b1c8f6897bec}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="147"/>
<source>Set LED brightness</source>
<extracomment>The name of the ActionType ({595908c1-57b1-4303-a0ca-4c64f3cb1907}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="150"/>
<source>Set Sleepmode</source>
<extracomment>The name of the ActionType ({19bd1456-2e4f-444a-a586-75bf6cc9fb73}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="153"/>
<source>Set Sleeptimer</source>
<extracomment>The name of the ActionType ({4c7594e4-70e7-4f0c-aae4-02e3993ffa1d}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="156"/>
<source>Set repeat</source>
<extracomment>The name of the ActionType ({27b5ff3b-bd60-411f-b8e3-b1c8f6897bec}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="159"/>
<source>Set volume</source>
<extracomment>The name of the ActionType ({93a5098a-a41a-46ee-8613-266d4f9ed69a}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="162"/>
<source>Signal strength</source>
<extracomment>The name of the StateType ({57bd1bab-d872-4315-b53e-1157fe3889d4}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="165"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="168"/>
<source>Sleepmode</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: sleepmode, ID: {19bd1456-2e4f-444a-a586-75bf6cc9fb73})
----------
The name of the StateType ({19bd1456-2e4f-444a-a586-75bf6cc9fb73}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="171"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="174"/>
<source>Sleeptimer</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: sleeptimer, ID: {4c7594e4-70e7-4f0c-aae4-02e3993ffa1d})
----------
The name of the StateType ({4c7594e4-70e7-4f0c-aae4-02e3993ffa1d}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="177"/>
<source>Stop</source>
<extracomment>The name of the ActionType ({d46f0b61-d406-4302-adc3-6bbc00fc2a8f}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="180"/>
<source>Title</source>
<extracomment>The name of the StateType ({a274e048-9820-444a-b5de-a3a421c855a2}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="183"/>
<location filename="../../../build-nymea-plugins-Desktop-Debug/espuino/plugininfo.h" line="186"/>
<source>Volume</source>
<extracomment>The name of the ParamType (ThingClass: espuino, ActionType: volume, ID: {93a5098a-a41a-46ee-8613-266d4f9ed69a})
----------
The name of the StateType ({93a5098a-a41a-46ee-8613-266d4f9ed69a}) of ThingClass espuino</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>IntegrationPluginEspuino</name>
<message>
<location filename="../integrationpluginespuino.cpp" line="92"/>
<location filename="../integrationpluginespuino.cpp" line="154"/>
<source>Error creating MQTT channel. Please check MQTT server settings.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../integrationpluginespuino.cpp" line="117"/>
<location filename="../integrationpluginespuino.cpp" line="123"/>
<source>Failed to configure MQTT via Websocket.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View File

@ -21,6 +21,7 @@ PLUGIN_DIRS = \
dynatrace \
elgato \
eq-3 \
espuino \
fastcom \
flowercare \
fronius \