Added new sensors plugin to webpanel

This commit is contained in:
Fabio Manganiello 2019-07-02 14:04:25 +02:00
parent b932df1c12
commit 1eae45805d
21 changed files with 203 additions and 1120 deletions

View File

@ -0,0 +1,5 @@
@import 'common/vars';
.sensors {
}

View File

@ -1,756 +0,0 @@
$(document).ready(function() {
var $container = $('#video-container'),
$searchForm = $('#video-search'),
$videoResults = $('#video-results'),
$volumeCtrl = $('#video-volume-ctrl'),
$ctrlForm = $('#video-ctrl'),
$devsPanel = $('#media-devices-panel'),
$devsList = $devsPanel.find('.devices-list'),
$devsBtn = $('button[data-panel="#media-devices-panel"]'),
$devsBtnIcon = $('#media-devices-panel-icon'),
$devsRefreshBtn = $devsPanel.find('.refresh-devices'),
$searchBarContainer = $('#media-search-bar-container'),
$mediaBtnsContainer = $('#media-btns-container'),
$mediaItemPanel = $('#media-item-panel'),
$mediaSubtitlesModal = $('#media-subtitles-modal'),
$mediaSubtitlesResultsContainer = $mediaSubtitlesModal.find('.media-subtitles-results-container'),
$mediaSubtitlesResults = $mediaSubtitlesModal.find('.media-subtitles-results'),
$mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'),
$mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'),
prevVolume = undefined,
selectedResource = undefined,
browserVideoWindow = undefined,
browserVideoElement = undefined;
const onEvent = (event) => {
switch (event.args.type) {
case 'platypush.message.event.media.MediaPlayRequestEvent':
createNotification({
'icon': 'stream',
'html': 'Processing media' + ('resource' in event.args
? ' ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPlayEvent':
createNotification({
'icon': 'play',
'html': 'Starting media playback' + ('resource' in event.args
? ' for ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPauseEvent':
createNotification({
'icon': 'pause',
'html': 'Media playback paused',
});
break;
case 'platypush.message.event.media.MediaStopEvent':
createNotification({
'icon': 'stop',
'html': 'Media playback stopped',
});
$mediaSearchSubtitles.hide();
break;
}
};
const updateVideoResults = function(videos) {
$videoResults.html('');
for (var video of videos) {
var $videoResult = $('<div></div>')
.addClass('video-result')
.attr('data-url', video['url'])
.attr('data-panel', '#media-item-panel')
.html('title' in video ? video['title'] : video['url']);
var $icon = getVideoIconByUrl(video['url']);
$icon.prependTo($videoResult);
$videoResult.appendTo($videoResults);
}
};
const getVideoIconByUrl = function(url) {
var $icon = $('<i class="fa"></i>');
if (url.startsWith('file://')) {
$icon.addClass('fa-download');
} else if (url.startsWith('https://www.youtube.com/')) {
$icon.addClass('fa-youtube');
} else if (url.startsWith('magnet:?')) {
$icon.addClass('fa-film');
} else {
$icon.addClass('fa-video');
}
var $iconContainer = $('<span></span>').addClass('video-icon-container');
$icon.appendTo($iconContainer);
return $iconContainer;
};
const getSelectedDevice = function() {
var device = { isBrowser: false, isRemote: false, name: undefined };
var $remoteDevice = $devsPanel.find('.cast-device.selected')
.filter((i, dev) => !$(dev).data('local') && !$(dev).data('browser') && $(dev).data('name'));
var $browserDevice = $devsPanel.find('.cast-device.selected')
.filter((i, dev) => $(dev).data('browser'));
if ($remoteDevice.length) {
device.isRemote = true;
device.name = $remoteDevice.data('name');
} else if ($browserDevice.length) {
device.isBrowser = true;
}
return device;
};
const startStreamingTorrent = function(torrent) {
// TODO support for subtitles download on torrent metadata received
return new Promise((resolve, reject) => {
execute(
{
type: 'request',
action: 'media.webtorrent.play',
args: {
resource: torrent,
download_only: true,
}
},
(response) => {
resolve(response.response.output.url);
},
(error) => {
reject(error);
}
);
});
};
const startStreaming = function(media, subtitles, relativeURIs) {
if (media.startsWith('magnet:?')) {
return new Promise((resolve, reject) => {
startStreamingTorrent(media)
.then((url) => { resolve({'url':url}); })
.catch((error) => { reject(error); });
});
}
return new Promise((resolve, reject) => {
$.ajax({
type: 'PUT',
url: '/media',
contentType: 'application/json',
data: JSON.stringify({
'source': media,
'subtitles': subtitles,
})
}).done((response) => {
var url = response.url;
var subs;
if ('subtitles_url' in response) {
subs = response.subtitles_url;
}
if (relativeURIs) {
url = url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1];
if (subs) {
subs = subs.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1];
}
}
var ret = { 'url': url, 'subtitles': undefined };
if (subs) {
ret.subtitles = subs;
}
resolve(ret);
}).fail((xhr) => {
reject(xhr.responseText);
});
});
};
const stopStreaming = function(media_id) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'DELETE',
url: '/media/' + media_id,
contentType: 'application/json',
});
});
};
const getSubtitles = function(resource) {
return new Promise((resolve, reject) => {
if (!window.config.media.subtitles || resource.startsWith('magnet:?')) {
resolve(); // media.subtitles plugin not configured
}
run({
action: 'media.subtitles.get_subtitles',
args: { 'resource': resource }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const downloadSubtitles = function(link, mediaResource, vtt=false) {
return new Promise((resolve, reject) => {
run({
action: 'media.subtitles.download',
args: {
'link': link,
'media_resource': mediaResource,
'convert_to_vtt': vtt,
}
}).then((response) => {
resolve(response.response.output.filename);
}).catch((error) => {
reject(error.message);
});
});
};
const setSubtitles = (filename) => {
return new Promise((resolve, reject) => {
run({
action: 'media.set_subtitles',
args: { 'filename': filename }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const playOnChromecast = (resource, device, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.chromecast.play',
args: {
'chromecast': device,
},
};
startStreaming(resource, subtitles).then((response) => {
requestArgs.args.resource = response.url;
// XXX subtitles currently break the Chromecast playback,
// see https://github.com/balloob/pychromecast/issues/74
// if (response.subtitles) {
// requestArgs.args.subtitles = response.subtitles;
// }
return run(requestArgs);
}).then((response) => {
if ('subtitles' in requestArgs) {
resolve(requestArgs.args.resource, requestArgs.args.subtitles);
} else {
resolve(requestArgs.args.resource);
}
}).catch((error) => {
reject(error);
});
});
};
const playInBrowser = (resource, subtitles) => {
return new Promise((resolve, reject) => {
startStreaming(resource, subtitles, true).then((response) => {
browserVideoWindow = window.open(
response.url + '?webplayer', '_blank');
browserVideoWindow.addEventListener('load', () => {
browserVideoElement = browserVideoWindow.document
.querySelector('#video-player');
});
resolve(response.url, response.subtitles);
}).catch((error) => {
reject(error);
});
});
};
const playOnServer = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.play',
args: { 'resource': resource },
};
if (subtitles) {
requestArgs.args.subtitles = subtitles;
}
run(requestArgs).then((response) => {
resolve(resource);
}).catch((error) => {
reject(error.message);
});
});
};
const _play = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var playHndl;
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
playHndl = playInBrowser(resource, subtitles);
} else if (selectedDevice.isRemote) {
playHndl = playOnChromecast(resource, selectedDevice.name, subtitles);
} else {
playHndl = playOnServer(resource, subtitles);
}
playHndl.then((response) => {
resolve(resource);
}).catch((error) => {
showError('Playback error: ' + (error ? error : 'undefined'));
reject(error);
});
});
};
const play = (resource) => {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = () => { $videoResults.text('Loading video...'); };
var onVideoReady = () => {
$videoResults.html(results);
resolve(resource);
};
var defaultPlay = () => { _play(resource).finally(onVideoReady); };
$mediaSearchSubtitles.data('resource', resource);
onVideoLoading();
var subtitlesConf = window.config.media.subtitles;
if (subtitlesConf) {
populateSubtitlesModal(resource).then((subs) => {
if ('language' in subtitlesConf) {
if (subs && subs.length) {
downloadSubtitles(subs[0].SubDownloadLink, resource).then((subtitles) => {
_play(resource, subtitles).finally(onVideoReady);
resolve(resource, subtitles);
}).catch((error) => {
defaultPlay();
resolve(resource);
});
} else {
defaultPlay();
resolve(resource);
}
} else {
defaultPlay();
resolve(resource);
}
});
} else {
defaultPlay();
resolve(resource);
}
});
};
const download = function(resource) {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = function() {
$videoResults.text('Loading video...');
};
var onVideoReady = function() {
$videoResults.html(results);
};
onVideoLoading();
startStreaming(resource, undefined, true)
.then((response) => {
var url = response.url + '?download'
window.open(url, '_blank');
resolve(url);
})
.catch((error) => {
reject(error);
})
.finally(() => {
onVideoReady();
});
});
};
const populateSubtitlesModal = (resource) => {
return new Promise((resolve, reject) => {
$mediaSubtitlesMessage.text('Loading subtitles...');
$mediaSubtitlesResults.text('');
$mediaSubtitlesMessage.show();
$mediaSubtitlesResultsContainer.hide();
getSubtitles(resource).then((subs) => {
if (!subs || !subs.length) {
$mediaSubtitlesMessage.text('No subtitles found');
resolve();
}
$mediaSearchSubtitles.show();
for (var sub of subs) {
var flagCode;
if ('ISO639' in sub) {
switch(sub.ISO639) {
case 'en': flagCode = 'gb'; break;
default: flagCode = sub.ISO639; break;
}
}
var $subContainer = $('<div></div>').addClass('row media-subtitle-container')
.data('download-link', sub.SubDownloadLink)
.data('resource', resource);
var $subFlagIconContainer = $('<div></div>').addClass('one column');
var $subFlagIcon = $('<span></span>')
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
sub.IsLocal ? 'fa fa-download' : ''))
.text(!(flagCode || sub.IsLocal) ? '?' : '');
var $subMovieName = $('<div></div>').addClass('five columns')
.text(sub.MovieName);
var $subFileName = $('<div></div>').addClass('six columns')
.text(sub.SubFileName);
$subFlagIcon.appendTo($subFlagIconContainer);
$subFlagIconContainer.appendTo($subContainer);
$subMovieName.appendTo($subContainer);
$subFileName.appendTo($subContainer);
$subContainer.appendTo($mediaSubtitlesResults);
}
$mediaSubtitlesMessage.hide();
$mediaSubtitlesResultsContainer.show();
resolve(subs);
}).catch((error) => {
$mediaSubtitlesMessage.text('Unable to load subtitles: ' + error);
reject(error);
});
});
};
const setBrowserPlayerSubs = (resource, subtitles) => {
var mediaId;
if (!browserVideoElement) {
showError('No video is currently playing in the browser');
return;
}
return new Promise((resolve, reject) => {
$.get('/media').then((media) => {
for (var m of media) {
if (m.source === resource) {
mediaId = m.media_id;
break;
}
}
if (!mediaId) {
reject(resource + ' is not a registered media');
return;
}
return $.ajax({
type: 'POST',
url: '/media/subtitles/' + mediaId + '.vtt',
contentType: 'application/json',
data: JSON.stringify({
'filename': subtitles,
}),
});
}).then(() => {
resolve(resource, subtitles);
}).catch((error) => {
reject('Cannot set subtitles for ' + resource + ': ' + error);
});
});
};
const setLocalPlayerSubs = (resource, subtitles) => {
return new Promise((resolve, reject) => {
run({
action: 'media.remove_subtitles'
}).then((response) => {
return setSubtitles(subtitles);
}).then((response) => {
resolve(response);
}).catch((error) => {
reject(error);
});
});
};
const initBindings = function() {
window.registerEventListener(onEvent);
$searchForm.on('submit', function(event) {
var $input = $(this).find('input[name=video-search-text]');
var resource = $input.val();
var request = {}
var onSuccess = function() {};
var onError = function() {};
var onComplete = function() {
$input.prop('disabled', false);
};
$input.prop('disabled', true);
$mediaItemPanel.find('[data-action]').removeClass('disabled');
$videoResults.text('Searching...');
if (resource.match(new RegExp('^https?://')) ||
resource.match(new RegExp('^file://'))) {
var videos = [{ url: resource }];
updateVideoResults(videos);
request = {
type: 'request',
action: 'media.play',
args: { resource: resource }
};
} else {
request = {
type: 'request',
action: 'media.search',
args: { query: resource }
};
onSuccess = function(response) {
var videos = response.response.output;
updateVideoResults(videos);
};
}
execute(request, onSuccess, onError, onComplete)
return false;
});
$ctrlForm.on('submit', function() { return false; });
$ctrlForm.find('button[data-action]').on('click touch', function(evt) {
var action = $(this).data('action');
var $btn = $(this);
var requestArgs = {
type: 'request',
action: 'media.' + action,
};
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
return; // The in-browser player can be used to control media
}
if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.' + action;
requestArgs.args = { 'chromecast': selectedDevice.name };
}
execute(requestArgs);
});
$volumeCtrl.on('mousedown touchstart', function(event) {
prevVolume = $(this).val();
});
$volumeCtrl.on('mouseup touchend', function(event) {
var requestArgs = {
type: 'request',
action: 'media.set_volume',
args: { volume: $(this).val() },
};
var selectedDevice = getSelectedDevice();
if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.set_volume',
requestArgs.args.chromecast = selectedDevice.name;
}
execute(requestArgs,
onSuccess=undefined,
onError = function() {
$volumeCtrl.val(prevVolume);
}
);
});
$videoResults.on('mousedown touchstart', '.video-result', function() {
selectedResource = $(this).data('url');
});
$videoResults.on('mouseup touchend', '.video-result', function(event) {
var $item = $(this);
var resource = $item.data('url');
if (resource !== selectedResource) {
return; // Did not really click this item
}
$item.siblings().removeClass('selected');
$item.addClass('selected');
$mediaItemPanel.css('top', (event.clientY + $(window).scrollTop()) + 'px');
$mediaItemPanel.css('left', (event.clientX + $(window).scrollLeft()) + 'px');
$mediaItemPanel.data('resource', resource);
});
$devsBtn.on('click touch', function() {
$(this).toggleClass('selected');
$devsPanel.css('top', ($(this).position().top + $(this).outerHeight()) + 'px');
$devsPanel.css('left', ($(this).position().left) + 'px');
return false;
});
$devsPanel.on('mouseup touchend', '.cast-device', function() {
if ($(this).hasClass('disabled')) {
return;
}
var $devices = $devsPanel.find('.cast-device');
var $curSelected = $devices.filter((i, d) => $(d).hasClass('selected'));
if ($curSelected.data('name') !== $(this).data('name')) {
$curSelected.removeClass('selected');
$(this).addClass('selected');
$devsBtnIcon.attr('class', $(this).find('.fa').attr('class'));
if ($(this).data('browser') || $(this).data('local')) {
$devsBtn.removeClass('remote');
} else {
$devsBtn.addClass('remote');
}
// TODO Logic for switching destination on the fly
}
$devsPanel.hide();
$devsBtn.removeClass('selected');
});
$devsRefreshBtn.on('click', function() {
if ($(this).hasClass('disabled')) {
return;
}
$(this).addClass('disabled');
initRemoteDevices();
});
$mediaItemPanel.on('click', '[data-action]', function() {
var $action = $(this);
if ($action.hasClass('disabled')) {
return;
}
var action = $action.data('action');
var resource = $mediaItemPanel.data('resource');
if (!resource) {
return;
}
$mediaItemPanel.hide();
$mediaItemPanel.find('[data-action]').addClass('disabled');
eval(action)($mediaItemPanel.data('resource'))
.finally(() => {
$mediaItemPanel.find('[data-action]').removeClass('disabled');
});
});
$mediaSubtitlesModal.on('mouseup touchend', '.media-subtitle-container', (event) => {
var resource = $(event.currentTarget).data('resource');
var link = $(event.currentTarget).data('downloadLink');
var selectedDevice = getSelectedDevice();
if (selectedDevice.isRemote) {
showError('Changing subtitles at runtime on Chromecast is not yet supported');
return;
}
var convertToVTT = selectedDevice.isBrowser;
downloadSubtitles(link, resource, convertToVTT).then((subtitles) => {
if (selectedDevice.isBrowser) {
return setBrowserPlayerSubs(resource, subtitles);
} else {
return setLocalPlayerSubs(resource, subtitles);
}
}).catch((error) => {
console.warning('Could not load subtitles ' + link +
' to the player: ' + (error || 'undefined error'))
});
});
};
const initRemoteDevices = function() {
$devsList.find('.cast-device[data-remote]').addClass('disabled');
execute(
{
type: 'request',
action: 'media.chromecast.get_chromecasts',
},
onSuccess = function(results) {
$devsList.find('.cast-device[data-remote]').remove();
$devsRefreshBtn.removeClass('disabled');
if (!results || results.response.errors.length) {
return;
}
results = results.response.output;
for (var cast of results) {
var $cast = $('<div></div>').addClass('row cast-device')
.addClass('cast-device-' + cast.type).attr('data-remote', true)
.data('name', cast.name);
var icon = 'question';
switch (cast.type) {
case 'cast': icon = 'tv'; break;
case 'audio': icon = 'volume-up'; break;
}
var $castIcon = $('<i></i>').addClass('fa fa-' + icon)
.addClass('cast-device-icon');
var $castName = $('<span></span>').addClass('cast-device-name')
.text(cast.name);
var $iconContainer = $('<div></div>').addClass('two columns');
var $nameContainer = $('<div></div>').addClass('ten columns');
$castIcon.appendTo($iconContainer);
$castName.appendTo($nameContainer);
$iconContainer.appendTo($cast);
$nameContainer.appendTo($cast);
$cast.appendTo($devsList);
}
},
onComplete = function() {
$devsRefreshBtn.removeClass('disabled');
}
);
};
const init = function() {
initRemoteDevices();
initBindings();
};
init();
});

View File

@ -0,0 +1,61 @@
Vue.component('sensor-metric', {
template: '#tmpl-sensor-metric',
props: {
bus: {
type: Object,
},
device: {
type: Object,
default: () => {},
},
},
});
Vue.component('sensors', {
template: '#tmpl-sensors',
props: ['config'],
data: function() {
return {
bus: new Vue({}),
metrics: {},
};
},
methods: {
refresh: async function() {
if (!this.config.plugins) {
console.warn('Please specify a list of sensor plugins in your sensors section configuration');
return;
}
const promises = this.config.plugins.map(plugin => {
return new Promise((resolve, reject) => {
request(plugin + '.get_measurement').then(metrics => {
resolve(metrics);
});
});
});
this.metrics = (await Promise.all(promises)).reduce((obj, metrics) => {
for (const [name, value] of Object.entries(metrics)) {
obj[name] = value;
}
return obj;
}, {});
},
onSensorEvent: function(event) {
const data = event.data;
this.metrics[data.name] = data.value;
},
},
mounted: function() {
this.refresh();
registerEventHandler(this.onSensorEvent, 'platypush.message.event.sensor.SensorDataChangeEvent');
},
});

View File

@ -1,33 +0,0 @@
$(document).ready(function() {
var switches,
$switchbotContainer = $('#switchbot-container');
var initBindings = function() {
$switchbotContainer.on('click touch', '.switch-ctrl-container', function() {
var $input = $(this).find('.switch-ctrl');
var addr = $input.attr('name');
$input.prop('checked', true);
execute(
{
type: 'request',
action: 'switch.switchbot.press',
args: {
device: addr
}
},
onSuccess = function(response) {
$input.prop('checked', false);
}
);
});
};
var init = function() {
initBindings();
};
init();
});

View File

@ -1,77 +0,0 @@
$(document).ready(function() {
var switches,
$tplinkContainer = $('#tplink-container');
var createPowerToggleElement = function(dev) {
var $powerToggle = $('<div></div>').addClass('toggle toggle--push switch-ctrl-container');
var $input = $('<input></input>').attr('type', 'checkbox')
.data('name', dev.host).attr('name', dev.alias).addClass('toggle--checkbox switch-ctrl');
var $label = $('<label></label>').attr('for', dev.alias).addClass('toggle--btn');
$input.appendTo($powerToggle);
$label.appendTo($powerToggle);
if (dev.on) {
$input.prop('checked', true);
}
return $powerToggle;
};
var updateDevices = function(devices) {
for (var dev of devices) {
var $dev = $('<div></div>').addClass('row tplink-device').data('name', dev.alias);
var $devName = $('<div></div>').addClass('ten columns name').text(dev.alias);
var $toggleContainer = $('<div></div>').addClass('two columns toggle-container');
var $toggle = createPowerToggleElement(dev);
$toggle.appendTo($toggleContainer);
$devName.appendTo($dev);
$toggleContainer.appendTo($dev);
$dev.appendTo($tplinkContainer);
}
};
var initWidget = function() {
execute(
{
type: 'request',
action: 'switch.tplink.status'
},
onSuccess = function(response) {
updateDevices(Object.values(response.response.output.devices));
}
);
};
var initBindings = function() {
$tplinkContainer.on('click touch', '.switch-ctrl-container', function() {
var $input = $(this).find('.switch-ctrl');
var devAddr = $input.data('name');
var action = $input.prop('checked') ? 'off' : 'on';
execute(
{
type: 'request',
action: 'switch.tplink.' + action,
args: { device: devAddr }
},
onSuccess = function(response) {
var status = response.response.output.status;
$input.prop('checked', status == 'on' ? true : false);
}
);
});
};
var init = function() {
initWidget();
initBindings();
};
init();
});

View File

@ -1,78 +0,0 @@
$(document).ready(function() {
var switches,
$wemoContainer = $('#wemo-container');
var createPowerToggleElement = function(dev) {
var $powerToggle = $('<div></div>').addClass('toggle toggle--push switch-ctrl-container');
var $input = $('<input></input>').attr('type', 'checkbox')
.attr('name', dev.name).addClass('toggle--checkbox switch-ctrl');
var $label = $('<label></label>').attr('for', dev.name).addClass('toggle--btn');
$input.appendTo($powerToggle);
$label.appendTo($powerToggle);
if (dev.state == 1) {
$input.prop('checked', true);
}
return $powerToggle;
};
var updateDevices = function(devices) {
for (var dev of devices) {
var $dev = $('<div></div>').addClass('row wemo-device').data('name', dev.name);
var $devName = $('<div></div>').addClass('ten columns name').text(dev.name);
var $toggleContainer = $('<div></div>').addClass('two columns toggle-container');
var $toggle = createPowerToggleElement(dev);
$toggle.appendTo($toggleContainer);
$devName.appendTo($dev);
$toggleContainer.appendTo($dev);
$dev.appendTo($wemoContainer);
}
};
var initWidget = function() {
execute(
{
type: 'request',
action: 'switch.wemo.get_devices'
},
onSuccess = function(response) {
updateDevices(response.response.output.devices);
}
);
};
var initBindings = function() {
$wemoContainer.on('click touch', '.switch-ctrl-container', function() {
var $input = $(this).find('.switch-ctrl');
var devName = $input.attr('name');
execute(
{
type: 'request',
action: 'switch.wemo.toggle',
args: {
device: devName
}
},
onSuccess = function(response) {
var state = response.response.output.state;
$input.prop('checked', !!state);
}
);
});
};
var init = function() {
initWidget();
initBindings();
};
init();
});

View File

@ -1,34 +0,0 @@
$(document).ready(function() {
var $container = $('#tts-container'),
$ttsForm = $('#tts-form');
var initBindings = function() {
$ttsForm.on('submit', function(event) {
var formData = $(this).serializeArray().reduce(function(obj, item) {
var value = item.value.trim();
if (value.length > 0) {
obj[item.name] = item.value;
}
return obj;
}, {});
execute(
{
type: 'request',
action: 'tts.google.say',
args: formData,
}
);
return false;
});
};
var init = function() {
initBindings();
};
init();
});

View File

@ -1,34 +0,0 @@
$(document).ready(function() {
var $container = $('#tts-container'),
$ttsForm = $('#tts-form');
var initBindings = function() {
$ttsForm.on('submit', function(event) {
var formData = $(this).serializeArray().reduce(function(obj, item) {
var value = item.value.trim();
if (value.length > 0) {
obj[item.name] = item.value;
}
return obj;
}, {});
execute(
{
type: 'request',
action: 'tts.say',
args: formData,
}
);
return false;
});
};
var init = function() {
initBindings();
};
init();
});

View File

@ -7,6 +7,7 @@
'media.vlc': 'fa fa-film',
'music.mpd': 'fa fa-music',
'music.snapcast': 'fa fa-volume-up',
'sensors': 'fa fa-temperature-half',
'switches': 'fa fa-toggle-on',
'tts': 'fa fa-comment',
'tts.google': 'fa fa-comment',

View File

@ -0,0 +1,24 @@
<script type="text/x-template" id="tmpl-sensors">
<div class="sensors">
<div class="head">
<button type="button" @click="refresh">
<i class="fa fa-sync"></i>
</button>
</div>
<div class="body">
<sensor-metric v-for="metric in metrics"
:key="metric.name"
:name="metric.name"
:value="metric.value">
</sensor-metric>
</div>
</div>
</script>
<script type="text/x-template" id="tmpl-sensor-metric">
<div class="metric">
<div class="col-6 name" v-text="name"></div>
<div class="col-6 value" v-text="value"></div>
</div>
</script>

View File

@ -12,13 +12,18 @@ class SensorBackend(Backend):
Triggers:
* :class:`platypush.message.event.sensor.SensorDataChangeEvent` if the measurements of a sensor have changed
* :class:`platypush.message.event.sensor.SensorDataAboveThresholdEvent` if the measurements of a sensor have gone above a configured threshold
* :class:`platypush.message.event.sensor.SensorDataBelowThresholdEvent` if the measurements of a sensor have gone below a configured threshold
* :class:`platypush.message.event.sensor.SensorDataAboveThresholdEvent` if the measurements of a sensor have
gone above a configured threshold
* :class:`platypush.message.event.sensor.SensorDataBelowThresholdEvent` if the measurements of a sensor have
gone below a configured threshold
"""
def __init__(self, thresholds=None, poll_seconds=None, *args, **kwargs):
def __init__(self, thresholds=None, poll_seconds=None, **kwargs):
"""
:param thresholds: Thresholds can be either a scalar value or a dictionary (e.g. ``{"temperature": 20.0}``). Sensor threshold events will be fired when measurements get above or below these values. Set it as a scalar if your get_measurement() code returns a scalar, as a dictionary if it returns a dictionary of values.
:param thresholds: Thresholds can be either a scalar value or a dictionary (e.g. ``{"temperature": 20.0}``).
Sensor threshold events will be fired when measurements get above or below these values.
Set it as a scalar if your get_measurement() code returns a scalar, as a dictionary if it returns a
dictionary of values.
For instance, if your sensor code returns both humidity and
temperature in a format like ``{'humidity':60.0, 'temperature': 25.0}``,
@ -26,7 +31,8 @@ class SensorBackend(Backend):
``{'temperature':20.0}`` to trigger events when the temperature goes
above/below 20 degrees.
:param poll_seconds: If set, the thread will wait for the specificed number of seconds between a read and the next one.
:param poll_seconds: If set, the thread will wait for the specified number of seconds between a read and the
next one.
:type poll_seconds: float
"""
@ -37,7 +43,7 @@ class SensorBackend(Backend):
self.poll_seconds = poll_seconds
def get_measurement(self):
""" To be implemented in the derived classes """
""" To be implemented by derived classes """
raise NotImplementedError('To be implemented in a derived class')
def run(self):
@ -75,7 +81,6 @@ class SensorBackend(Backend):
if data_above_threshold:
self.bus.post(SensorDataAboveThresholdEvent(data=data_above_threshold))
self.data = new_data
if self.poll_seconds:
@ -83,4 +88,3 @@ class SensorBackend(Backend):
# vim:sw=4:ts=4:et:

View File

@ -42,11 +42,7 @@ class SensorLeapBackend(Backend):
_listener_proc = None
def __init__(self,
position_ranges=[
[-300.0, 300.0], # x axis
[25.0, 600.0], # y axis
[-300.0, 300.0], # z axis
],
position_ranges=None,
position_tolerance=0.0, # Position variation tolerance in %
frames_throttle_secs=None,
*args, **kwargs):
@ -76,11 +72,17 @@ class SensorLeapBackend(Backend):
super().__init__(*args, **kwargs)
if position_ranges is None:
position_ranges = [
[-300.0, 300.0], # x axis
[25.0, 600.0], # y axis
[-300.0, 300.0], # z axis
]
self.position_ranges = position_ranges
self.position_tolerance = position_tolerance
self.frames_throttle_secs = frames_throttle_secs
def run(self):
super().run()
@ -123,8 +125,8 @@ class LeapFuture(Timer):
class LeapListener(Leap.Listener):
def __init__(self, position_ranges, position_tolerance, logger,
frames_throttle_secs=None, *args, **kwargs):
super().__init__(*args, **kwargs)
frames_throttle_secs=None):
super().__init__()
self.prev_frame = None
self.position_ranges = position_ranges
@ -133,26 +135,24 @@ class LeapListener(Leap.Listener):
self.logger = logger
self.running_future = None
def _send_event(self, event):
backend = get_backend('redis')
if not backend:
self.logger.warning('Redis backend not configured, I cannot propagate the following event: {}'.format(event))
self.logger.warning('Redis backend not configured, I cannot propagate the following event: {}'.
format(event))
return
backend.send_message(event)
def send_event(self, event):
if self.frames_throttle_secs:
if not self.running_future or not self.running_future.is_alive():
self.running_future = LeapFuture(seconds=self.frames_throttle_secs,
listener=self, event=event)
listener=self, event=event)
self.running_future.start()
else:
self._send_event(event)
def on_init(self, controller):
self.prev_frame = None
self.logger.info('Leap controller listener initialized')
@ -216,7 +216,7 @@ class LeapListener(Leap.Listener):
]
def _normalize_position(self, position):
# Normalize absolute position onto a semisphere centered in (0,0)
# Normalize absolute position onto a hemisphere centered in (0,0)
# having x_range = z_range = [-100, 100], y_range = [0, 100]
return [
@ -225,13 +225,15 @@ class LeapListener(Leap.Listener):
self._scale_scalar(value=position[2], range=self.position_ranges[2], new_range=[-100.0, 100.0]),
]
@staticmethod
def _scale_scalar(value, range, new_range):
if value < range[0]:
value=range[0]
if value > range[1]:
value=range[1]
def _scale_scalar(self, value, range, new_range):
if value < range[0]: value=range[0]
if value > range[1]: value=range[1]
return ((new_range[1]-new_range[0])/(range[1]-range[0]))*(value-range[0]) + new_range[0]
def _position_changed(self, old_position, new_position):
return (
abs(old_position[0]-new_position[0]) > self.position_tolerance or
@ -240,4 +242,3 @@ class LeapListener(Leap.Listener):
# vim:sw=4:ts=4:et:

View File

@ -20,4 +20,3 @@ class SensorMcp3008Backend(SensorBackend):
# vim:sw=4:ts=4:et:

View File

@ -13,10 +13,9 @@ class SensorSerialBackend(SensorBackend):
"""
def get_measurement(self):
""" Implemnetation of ``get_measurement`` """
""" Implementation of ``get_measurement`` """
plugin = get_plugin('serial')
return plugin.get_data().output
# vim:sw=4:ts=4:et:

View File

@ -47,4 +47,3 @@ class SensorDataBelowThresholdEvent(Event):
# vim:sw=4:ts=4:et:

View File

@ -2,9 +2,6 @@
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
"""
import threading
import time
from platypush.plugins import Plugin, action
@ -16,7 +13,7 @@ class GpioPlugin(Plugin):
* **RPi.GPIO** (`pip install RPi.GPIO`)
"""
def __init__(self, pins=None, *args, **kwargs):
def __init__(self, pins=None, **kwargs):
"""
:param pins: Configuration for the GPIO PINs as a name -> pin_number map.
:type pins: dict
@ -31,10 +28,10 @@ class GpioPlugin(Plugin):
}
"""
super().__init__(*args, **kwargs)
super().__init__(**kwargs)
self.pins_by_name = pins if pins else {}
self.pins_by_number = { number:name
for (name, number) in self.pins_by_name.items() }
self.pins_by_number = {number: name
for (name, number) in self.pins_by_name.items()}
def _get_pin_number(self, pin):
try:
@ -46,99 +43,106 @@ class GpioPlugin(Plugin):
return pin
@action
def write(self, pin, val):
def write(self, pin, value, name=None):
"""
Write a byte value to a pin.
:param pin: PIN number or configured name
:type pin: int or str
:param val: Value to write
:type val: int
:param name: Optional name for the written value (e.g. "temperature" or "humidity")
:type name: str
:param value: Value to write
:type value: int
:returns: dict
Response::
output = {
"name": <pin or metric name>,
"pin": <pin>,
"val": <val>,
"value": <value>,
"method": "write"
}
"""
import RPi.GPIO as gpio
name = name or pin
pin = self._get_pin_number(pin)
gpio.setmode(gpio.BCM)
gpio.setup(pin, gpio.OUT)
gpio.output(pin, val)
gpio.output(pin, value)
return {
'name': name,
'pin': pin,
'val': val,
'value': value,
'method': 'write',
}
@action
def read(self, pin):
def read(self, pin, name=None):
"""
Reads a value from a PIN.
:param pin: PIN number or configured name.
:type pin: int or str
:param name: Optional name for the read value (e.g. "temperature" or "humidity")
:type name: str
:returns: dict
Response::
output = {
"name": <pin number or pin/metric name>,
"pin": <pin>,
"val": <val>,
"value": <value>,
"method": "read"
}
"""
import RPi.GPIO as gpio
name = name or pin
pin = self._get_pin_number(pin)
gpio.setmode(gpio.BCM)
gpio.setup(pin, gpio.IN)
val = gpio.input(pin)
return {
'name': name,
'pin': pin,
'val': val,
'value': val,
'method': 'read',
}
@action
def get_measurement(self, pin=None):
if pin is None:
return self.read_all()
return self.read(pin)
@action
def read_all(self):
"""
Reads the values from all the configured PINs and returns them as a list. It will raise a RuntimeError if no PIN mappings were configured.
:returns: list
Reads the values from all the configured PINs and returns them as a list. It will raise a RuntimeError if no
PIN mappings were configured.
"""
import RPi.GPIO as gpio
if not self.pins_by_number:
raise RuntimeError("No PIN mappings were provided/configured")
values = []
for (pin, name) in self.pins_by_number.items():
gpio.setmode(gpio.BCM)
gpio.setup(pin, gpio.IN)
val = gpio.input(pin)
values.append({
'pin': pin,
'name': name,
'val': val,
})
values.append(self.read(pin=pin, name=name).output)
return values
# vim:sw=4:ts=4:et:

View File

@ -1,6 +1,3 @@
import threading
import time
from platypush.plugins import action
from platypush.plugins.gpio.sensor import GpioSensorPlugin
@ -23,7 +20,8 @@ class GpioSensorAccelerometerPlugin(GpioSensorPlugin):
:param g: Accelerometer range as a multiple of G - can be 2G, 4G, 8G or 16G
:type g: int
:param precision: If set, the position values will be rounded to the specified number of decimal digits (default: no rounding)
:param precision: If set, the position values will be rounded to the specified number of decimal digits
(default: no rounding)
:type precision: int
"""
@ -45,13 +43,13 @@ class GpioSensorAccelerometerPlugin(GpioSensorPlugin):
self.sensor = LIS3DH()
self.sensor.setRange(self.g)
@action
def get_measurement(self):
"""
Extends :func:`.GpioSensorPlugin.get_measurement`
:returns: The sensor's current position as a dictionary with the three components (x,y,z) in degrees, each between -90 and 90
:returns: The sensor's current position as a dictionary with the three components (x,y,z) in degrees, each
between -90 and 90
"""
values = [
@ -59,8 +57,12 @@ class GpioSensorAccelerometerPlugin(GpioSensorPlugin):
for pos in (self.sensor.getX(), self.sensor.getY(), self.sensor.getZ())
]
return { 'x': values[0], 'y': values[1], 'z': values[2] }
return {
'name': 'position',
'value': {
'x': values[0], 'y': values[1], 'z': values[2]
}
}
# vim:sw=4:ts=4:et:

View File

@ -1,4 +1,3 @@
import threading
import time
from platypush.plugins import action

View File

@ -1,5 +1,4 @@
import enum
import time
from platypush.plugins import action
from platypush.plugins.gpio.sensor import GpioSensorPlugin
@ -105,14 +104,13 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
self.channels = channels if channels else {}
self.mcp = None
def _get_mcp(self):
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008
if self.mode == MCP3008Mode.SOFTWARE:
self.mcp = Adafruit_MCP3008.MCP3008(clk=self.CLK, cs=self.CS,
miso=self.MISO, mosi=self.MOSI)
miso=self.MISO, mosi=self.MOSI)
elif self.mode == MCP3008Mode.HARDWARE:
self.mcp = Adafruit_MCP3008.MCP3008(spi=SPI.SpiDev(self.spi_port, self.spi_device))
else:
@ -120,11 +118,9 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
return self.mcp
def _convert_to_voltage(self, value):
return (value * self.Vdd) / 1023.0 if value is not None else None
@action
def get_measurement(self):
"""
@ -169,4 +165,3 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
# vim:sw=4:ts=4:et:

View File

@ -4,7 +4,6 @@ import time
from platypush.plugins import Plugin, action
from platypush.context import get_plugin
from platypush.config import Config
class Direction(enum.Enum):
@ -16,6 +15,7 @@ class Direction(enum.Enum):
DIR_AUTO_TOGGLE = 'auto_toggle'
# noinspection PyPep8Naming
class GpioZeroborgPlugin(Plugin):
"""
ZeroBorg plugin. It allows you to control a ZeroBorg
@ -28,10 +28,12 @@ class GpioZeroborgPlugin(Plugin):
_direction = None
_init_in_progress = threading.Lock()
def __init__(self, directions = {}, *args, **kwargs):
def __init__(self, directions=None, **kwargs):
"""
:param directions: Configuration for the motor directions. A direction is basically a configuration of the power delivered to each motor to allow whichever object you're controlling (wheels, robotic arms etc.) to move in a certain direction. In my experience the ZeroBorg always needs a bit of calibration, depending on factory defaults and the mechanical properties of the load it controls.
:param directions: Configuration for the motor directions. A direction is basically a configuration of the
power delivered to each motor to allow whichever object you're controlling (wheels, robotic arms etc.) to
move in a certain direction. In my experience the ZeroBorg always needs a bit of calibration, depending on
factory defaults and the mechanical properties of the load it controls.
Example configuration that I use to control a simple 4WD robot::
@ -73,13 +75,23 @@ class GpioZeroborgPlugin(Plugin):
}
}
Note that the special direction "auto" can contain a configuration that allows your device to move autonomously based on the inputs it gets from some sensors. In this case, I set the sensors configuration (a list) to periodically poll a GPIO-based ultrasound distance sensor plugin. ``timeout`` says after how long a poll attempt should fail. The plugin package is specified through ``plugin`` (``gpio.sensor.distance``) in this case, note that the plugin must be configured as well in order to work). The ``threshold`` value says around which value your logic should trigger. In this case, threshold=400 (40 cm). When the distance value is above that threshold (``above_threshold_direction``), then go "up" (no obstacles ahead). Otherwise (``below_threshold_direction``), turn "left" (avoid the obstacle).
Note that the special direction "auto" can contain a configuration that allows your device to move autonomously
based on the inputs it gets from some sensors. In this case, I set the sensors configuration (a list) to
periodically poll a GPIO-based ultrasound distance sensor plugin. ``timeout`` says after how long a poll
attempt should fail. The plugin package is specified through ``plugin`` (``gpio.sensor.distance``) in this
case, note that the plugin must be configured as well in order to work). The ``threshold`` value says
around which value your logic should trigger. In this case, threshold=400 (40 cm). When the distance value
is above that threshold (``above_threshold_direction``), then go "up" (no obstacles ahead). Otherwise
(``below_threshold_direction``), turn "left" (avoid the obstacle).
:type directions: dict
"""
if directions is None:
directions = {}
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
super().__init__(*args, **kwargs)
super().__init__(**kwargs)
self.directions = directions
self.auto_mode = False
@ -90,8 +102,8 @@ class GpioZeroborgPlugin(Plugin):
self.zb.SetCommsFailsafe(True)
self.zb.ResetEpo()
def _get_measurement(self, plugin, timeout):
@staticmethod
def _get_measurement(plugin, timeout):
measure_start_time = time.time()
value = None
@ -123,11 +135,10 @@ class GpioZeroborgPlugin(Plugin):
direction = sensor['below_threshold_direction']
self.logger.info('Sensor: {}\tMeasurement: {}\tDirection: {}'
.format(sensor['plugin'], value, direction))
.format(sensor['plugin'], value, direction))
return direction
@action
def drive(self, direction):
"""
@ -140,17 +151,12 @@ class GpioZeroborgPlugin(Plugin):
"""
prev_direction = self._direction
self._can_run = True
self._direction = direction.lower()
self.logger.info('Received ZeroBorg drive command: {}'.format(direction))
def _run():
while self._can_run and self._direction:
left = 0.0
right = 0.0
if self._direction == Direction.DIR_AUTO_TOGGLE.value:
if self.auto_mode:
self._direction = None
@ -182,13 +188,11 @@ class GpioZeroborgPlugin(Plugin):
self.auto_mode = False
self._drive_thread = threading.Thread(target=_run)
self._drive_thread.start()
return {'status': 'running', 'direction': direction}
@action
def stop(self):
"""
@ -202,8 +206,7 @@ class GpioZeroborgPlugin(Plugin):
self.zb.MotorsOff()
self.zb.ResetEpo()
return {'status':'stopped'}
return {'status': 'stopped'}
# vim:sw=4:ts=4:et:

View File

@ -4,10 +4,11 @@ import serial
import threading
import time
from platypush.plugins import Plugin, action
from platypush.plugins import action
from platypush.plugins.gpio.sensor import GpioSensorPlugin
# noinspection PyBroadException
class SerialPlugin(GpioSensorPlugin):
"""
The serial plugin can read data from a serial device, as long as the serial
@ -35,8 +36,8 @@ class SerialPlugin(GpioSensorPlugin):
self.serial_lock = threading.Lock()
self.last_measurement = None
def _read_json(self, serial_port):
@staticmethod
def _read_json(serial_port):
n_brackets = 0
is_escaped_ch = False
parse_start = False
@ -122,10 +123,10 @@ class SerialPlugin(GpioSensorPlugin):
serial_available = self.serial_lock.acquire(timeout=2)
if serial_available:
try:
ser = self._get_serial(device=device)
ser = self._get_serial(device=device, baud_rate=baud_rate)
except:
time.sleep(1)
ser = self._get_serial(device=device, reset=True)
ser = self._get_serial(device=device, baud_rate=baud_rate, reset=True)
data = self._read_json(ser)
@ -147,7 +148,6 @@ class SerialPlugin(GpioSensorPlugin):
return data
@action
def read(self, device=None, baud_rate=None, size=None, end=None):
"""
@ -179,7 +179,7 @@ class SerialPlugin(GpioSensorPlugin):
if (size is None and end is None) or (size is not None and end is not None):
raise RuntimeError('Either size or end must be specified')
if end and len(end) > 1:
if end and isinstance(end, str) and len(end) > 1:
raise RuntimeError('The serial end must be a single character, not a string')
data = bytes()
@ -188,10 +188,10 @@ class SerialPlugin(GpioSensorPlugin):
serial_available = self.serial_lock.acquire(timeout=2)
if serial_available:
try:
ser = self._get_serial(device=device)
ser = self._get_serial(device=device, baud_rate=baud_rate)
except:
time.sleep(1)
ser = self._get_serial(device=device, reset=True)
ser = self._get_serial(device=device, baud_rate=baud_rate, reset=True)
if size is not None:
for _ in range(0, size):
@ -255,10 +255,10 @@ class SerialPlugin(GpioSensorPlugin):
serial_available = self.serial_lock.acquire(timeout=2)
if serial_available:
try:
ser = self._get_serial(device=device)
ser = self._get_serial(device=device, baud_rate=baud_rate)
except:
time.sleep(1)
ser = self._get_serial(device=device, reset=True)
ser = self._get_serial(device=device, baud_rate=baud_rate, reset=True)
self.logger.info('Writing {} to {}'.format(data, self.device))
ser.write(data)
@ -270,4 +270,3 @@ class SerialPlugin(GpioSensorPlugin):
# vim:sw=4:ts=4:et: