forked from platypush/platypush
Added new sensors plugin to webpanel
This commit is contained in:
parent
b932df1c12
commit
1eae45805d
21 changed files with 203 additions and 1120 deletions
|
@ -0,0 +1,5 @@
|
||||||
|
@import 'common/vars';
|
||||||
|
|
||||||
|
.sensors {
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
61
platypush/backend/http/static/js/plugins/sensors/index.js
Normal file
61
platypush/backend/http/static/js/plugins/sensors/index.js
Normal 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');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
'media.vlc': 'fa fa-film',
|
'media.vlc': 'fa fa-film',
|
||||||
'music.mpd': 'fa fa-music',
|
'music.mpd': 'fa fa-music',
|
||||||
'music.snapcast': 'fa fa-volume-up',
|
'music.snapcast': 'fa fa-volume-up',
|
||||||
|
'sensors': 'fa fa-temperature-half',
|
||||||
'switches': 'fa fa-toggle-on',
|
'switches': 'fa fa-toggle-on',
|
||||||
'tts': 'fa fa-comment',
|
'tts': 'fa fa-comment',
|
||||||
'tts.google': 'fa fa-comment',
|
'tts.google': 'fa fa-comment',
|
||||||
|
|
24
platypush/backend/http/templates/plugins/sensors/index.html
Normal file
24
platypush/backend/http/templates/plugins/sensors/index.html
Normal 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>
|
|
@ -12,13 +12,18 @@ class SensorBackend(Backend):
|
||||||
Triggers:
|
Triggers:
|
||||||
|
|
||||||
* :class:`platypush.message.event.sensor.SensorDataChangeEvent` if the measurements of a sensor have changed
|
* :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.SensorDataAboveThresholdEvent` if the measurements of a sensor have
|
||||||
* :class:`platypush.message.event.sensor.SensorDataBelowThresholdEvent` if the measurements of a sensor have gone below a configured threshold
|
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
|
For instance, if your sensor code returns both humidity and
|
||||||
temperature in a format like ``{'humidity':60.0, 'temperature': 25.0}``,
|
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
|
``{'temperature':20.0}`` to trigger events when the temperature goes
|
||||||
above/below 20 degrees.
|
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
|
:type poll_seconds: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -37,7 +43,7 @@ class SensorBackend(Backend):
|
||||||
self.poll_seconds = poll_seconds
|
self.poll_seconds = poll_seconds
|
||||||
|
|
||||||
def get_measurement(self):
|
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')
|
raise NotImplementedError('To be implemented in a derived class')
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
@ -75,7 +81,6 @@ class SensorBackend(Backend):
|
||||||
if data_above_threshold:
|
if data_above_threshold:
|
||||||
self.bus.post(SensorDataAboveThresholdEvent(data=data_above_threshold))
|
self.bus.post(SensorDataAboveThresholdEvent(data=data_above_threshold))
|
||||||
|
|
||||||
|
|
||||||
self.data = new_data
|
self.data = new_data
|
||||||
|
|
||||||
if self.poll_seconds:
|
if self.poll_seconds:
|
||||||
|
@ -83,4 +88,3 @@ class SensorBackend(Backend):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -42,11 +42,7 @@ class SensorLeapBackend(Backend):
|
||||||
_listener_proc = None
|
_listener_proc = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
position_ranges=[
|
position_ranges=None,
|
||||||
[-300.0, 300.0], # x axis
|
|
||||||
[25.0, 600.0], # y axis
|
|
||||||
[-300.0, 300.0], # z axis
|
|
||||||
],
|
|
||||||
position_tolerance=0.0, # Position variation tolerance in %
|
position_tolerance=0.0, # Position variation tolerance in %
|
||||||
frames_throttle_secs=None,
|
frames_throttle_secs=None,
|
||||||
*args, **kwargs):
|
*args, **kwargs):
|
||||||
|
@ -76,11 +72,17 @@ class SensorLeapBackend(Backend):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
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_ranges = position_ranges
|
||||||
self.position_tolerance = position_tolerance
|
self.position_tolerance = position_tolerance
|
||||||
self.frames_throttle_secs = frames_throttle_secs
|
self.frames_throttle_secs = frames_throttle_secs
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
super().run()
|
super().run()
|
||||||
|
|
||||||
|
@ -123,8 +125,8 @@ class LeapFuture(Timer):
|
||||||
|
|
||||||
class LeapListener(Leap.Listener):
|
class LeapListener(Leap.Listener):
|
||||||
def __init__(self, position_ranges, position_tolerance, logger,
|
def __init__(self, position_ranges, position_tolerance, logger,
|
||||||
frames_throttle_secs=None, *args, **kwargs):
|
frames_throttle_secs=None):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__()
|
||||||
|
|
||||||
self.prev_frame = None
|
self.prev_frame = None
|
||||||
self.position_ranges = position_ranges
|
self.position_ranges = position_ranges
|
||||||
|
@ -133,26 +135,24 @@ class LeapListener(Leap.Listener):
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.running_future = None
|
self.running_future = None
|
||||||
|
|
||||||
|
|
||||||
def _send_event(self, event):
|
def _send_event(self, event):
|
||||||
backend = get_backend('redis')
|
backend = get_backend('redis')
|
||||||
if not backend:
|
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
|
return
|
||||||
|
|
||||||
backend.send_message(event)
|
backend.send_message(event)
|
||||||
|
|
||||||
|
|
||||||
def send_event(self, event):
|
def send_event(self, event):
|
||||||
if self.frames_throttle_secs:
|
if self.frames_throttle_secs:
|
||||||
if not self.running_future or not self.running_future.is_alive():
|
if not self.running_future or not self.running_future.is_alive():
|
||||||
self.running_future = LeapFuture(seconds=self.frames_throttle_secs,
|
self.running_future = LeapFuture(seconds=self.frames_throttle_secs,
|
||||||
listener=self, event=event)
|
listener=self, event=event)
|
||||||
self.running_future.start()
|
self.running_future.start()
|
||||||
else:
|
else:
|
||||||
self._send_event(event)
|
self._send_event(event)
|
||||||
|
|
||||||
|
|
||||||
def on_init(self, controller):
|
def on_init(self, controller):
|
||||||
self.prev_frame = None
|
self.prev_frame = None
|
||||||
self.logger.info('Leap controller listener initialized')
|
self.logger.info('Leap controller listener initialized')
|
||||||
|
@ -216,7 +216,7 @@ class LeapListener(Leap.Listener):
|
||||||
]
|
]
|
||||||
|
|
||||||
def _normalize_position(self, position):
|
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]
|
# having x_range = z_range = [-100, 100], y_range = [0, 100]
|
||||||
|
|
||||||
return [
|
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]),
|
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]
|
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):
|
def _position_changed(self, old_position, new_position):
|
||||||
return (
|
return (
|
||||||
abs(old_position[0]-new_position[0]) > self.position_tolerance or
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -20,4 +20,3 @@ class SensorMcp3008Backend(SensorBackend):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,9 @@ class SensorSerialBackend(SensorBackend):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_measurement(self):
|
def get_measurement(self):
|
||||||
""" Implemnetation of ``get_measurement`` """
|
""" Implementation of ``get_measurement`` """
|
||||||
plugin = get_plugin('serial')
|
plugin = get_plugin('serial')
|
||||||
return plugin.get_data().output
|
return plugin.get_data().output
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -47,4 +47,3 @@ class SensorDataBelowThresholdEvent(Event):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,6 @@
|
||||||
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
|
.. moduleauthor:: Fabio Manganiello <blacklight86@gmail.com>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,7 +13,7 @@ class GpioPlugin(Plugin):
|
||||||
* **RPi.GPIO** (`pip install RPi.GPIO`)
|
* **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.
|
:param pins: Configuration for the GPIO PINs as a name -> pin_number map.
|
||||||
:type pins: dict
|
: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_name = pins if pins else {}
|
||||||
self.pins_by_number = { number:name
|
self.pins_by_number = {number: name
|
||||||
for (name, number) in self.pins_by_name.items() }
|
for (name, number) in self.pins_by_name.items()}
|
||||||
|
|
||||||
def _get_pin_number(self, pin):
|
def _get_pin_number(self, pin):
|
||||||
try:
|
try:
|
||||||
|
@ -46,99 +43,106 @@ class GpioPlugin(Plugin):
|
||||||
|
|
||||||
return pin
|
return pin
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def write(self, pin, val):
|
def write(self, pin, value, name=None):
|
||||||
"""
|
"""
|
||||||
Write a byte value to a pin.
|
Write a byte value to a pin.
|
||||||
|
|
||||||
:param pin: PIN number or configured name
|
:param pin: PIN number or configured name
|
||||||
:type pin: int or str
|
:type pin: int or str
|
||||||
|
|
||||||
:param val: Value to write
|
:param name: Optional name for the written value (e.g. "temperature" or "humidity")
|
||||||
:type val: int
|
:type name: str
|
||||||
|
|
||||||
|
:param value: Value to write
|
||||||
|
:type value: int
|
||||||
|
|
||||||
:returns: dict
|
:returns: dict
|
||||||
|
|
||||||
Response::
|
Response::
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
|
"name": <pin or metric name>,
|
||||||
"pin": <pin>,
|
"pin": <pin>,
|
||||||
"val": <val>,
|
"value": <value>,
|
||||||
"method": "write"
|
"method": "write"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import RPi.GPIO as gpio
|
import RPi.GPIO as gpio
|
||||||
|
|
||||||
|
name = name or pin
|
||||||
pin = self._get_pin_number(pin)
|
pin = self._get_pin_number(pin)
|
||||||
gpio.setmode(gpio.BCM)
|
gpio.setmode(gpio.BCM)
|
||||||
gpio.setup(pin, gpio.OUT)
|
gpio.setup(pin, gpio.OUT)
|
||||||
gpio.output(pin, val)
|
gpio.output(pin, value)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'name': name,
|
||||||
'pin': pin,
|
'pin': pin,
|
||||||
'val': val,
|
'value': value,
|
||||||
'method': 'write',
|
'method': 'write',
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def read(self, pin):
|
def read(self, pin, name=None):
|
||||||
"""
|
"""
|
||||||
Reads a value from a PIN.
|
Reads a value from a PIN.
|
||||||
|
|
||||||
:param pin: PIN number or configured name.
|
:param pin: PIN number or configured name.
|
||||||
:type pin: int or str
|
:type pin: int or str
|
||||||
|
|
||||||
|
:param name: Optional name for the read value (e.g. "temperature" or "humidity")
|
||||||
|
:type name: str
|
||||||
|
|
||||||
:returns: dict
|
:returns: dict
|
||||||
|
|
||||||
Response::
|
Response::
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
|
"name": <pin number or pin/metric name>,
|
||||||
"pin": <pin>,
|
"pin": <pin>,
|
||||||
"val": <val>,
|
"value": <value>,
|
||||||
"method": "read"
|
"method": "read"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import RPi.GPIO as gpio
|
import RPi.GPIO as gpio
|
||||||
|
|
||||||
|
name = name or pin
|
||||||
pin = self._get_pin_number(pin)
|
pin = self._get_pin_number(pin)
|
||||||
gpio.setmode(gpio.BCM)
|
gpio.setmode(gpio.BCM)
|
||||||
gpio.setup(pin, gpio.IN)
|
gpio.setup(pin, gpio.IN)
|
||||||
val = gpio.input(pin)
|
val = gpio.input(pin)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'name': name,
|
||||||
'pin': pin,
|
'pin': pin,
|
||||||
'val': val,
|
'value': val,
|
||||||
'method': 'read',
|
'method': 'read',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
def get_measurement(self, pin=None):
|
||||||
|
if pin is None:
|
||||||
|
return self.read_all()
|
||||||
|
return self.read(pin)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def read_all(self):
|
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.
|
Reads the values from all the configured PINs and returns them as a list. It will raise a RuntimeError if no
|
||||||
:returns: list
|
PIN mappings were configured.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import RPi.GPIO as gpio
|
|
||||||
|
|
||||||
if not self.pins_by_number:
|
if not self.pins_by_number:
|
||||||
raise RuntimeError("No PIN mappings were provided/configured")
|
raise RuntimeError("No PIN mappings were provided/configured")
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
for (pin, name) in self.pins_by_number.items():
|
for (pin, name) in self.pins_by_number.items():
|
||||||
gpio.setmode(gpio.BCM)
|
values.append(self.read(pin=pin, name=name).output)
|
||||||
gpio.setup(pin, gpio.IN)
|
|
||||||
val = gpio.input(pin)
|
|
||||||
|
|
||||||
values.append({
|
|
||||||
'pin': pin,
|
|
||||||
'name': name,
|
|
||||||
'val': val,
|
|
||||||
})
|
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
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
|
:param g: Accelerometer range as a multiple of G - can be 2G, 4G, 8G or 16G
|
||||||
:type g: int
|
: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
|
:type precision: int
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -45,13 +43,13 @@ class GpioSensorAccelerometerPlugin(GpioSensorPlugin):
|
||||||
self.sensor = LIS3DH()
|
self.sensor = LIS3DH()
|
||||||
self.sensor.setRange(self.g)
|
self.sensor.setRange(self.g)
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_measurement(self):
|
def get_measurement(self):
|
||||||
"""
|
"""
|
||||||
Extends :func:`.GpioSensorPlugin.get_measurement`
|
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 = [
|
values = [
|
||||||
|
@ -59,8 +57,12 @@ class GpioSensorAccelerometerPlugin(GpioSensorPlugin):
|
||||||
for pos in (self.sensor.getX(), self.sensor.getY(), self.sensor.getZ())
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import threading
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import enum
|
import enum
|
||||||
import time
|
|
||||||
|
|
||||||
from platypush.plugins import action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
||||||
|
@ -105,14 +104,13 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
|
||||||
self.channels = channels if channels else {}
|
self.channels = channels if channels else {}
|
||||||
self.mcp = None
|
self.mcp = None
|
||||||
|
|
||||||
|
|
||||||
def _get_mcp(self):
|
def _get_mcp(self):
|
||||||
import Adafruit_GPIO.SPI as SPI
|
import Adafruit_GPIO.SPI as SPI
|
||||||
import Adafruit_MCP3008
|
import Adafruit_MCP3008
|
||||||
|
|
||||||
if self.mode == MCP3008Mode.SOFTWARE:
|
if self.mode == MCP3008Mode.SOFTWARE:
|
||||||
self.mcp = Adafruit_MCP3008.MCP3008(clk=self.CLK, cs=self.CS,
|
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:
|
elif self.mode == MCP3008Mode.HARDWARE:
|
||||||
self.mcp = Adafruit_MCP3008.MCP3008(spi=SPI.SpiDev(self.spi_port, self.spi_device))
|
self.mcp = Adafruit_MCP3008.MCP3008(spi=SPI.SpiDev(self.spi_port, self.spi_device))
|
||||||
else:
|
else:
|
||||||
|
@ -120,11 +118,9 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
|
||||||
|
|
||||||
return self.mcp
|
return self.mcp
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_voltage(self, value):
|
def _convert_to_voltage(self, value):
|
||||||
return (value * self.Vdd) / 1023.0 if value is not None else None
|
return (value * self.Vdd) / 1023.0 if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_measurement(self):
|
def get_measurement(self):
|
||||||
"""
|
"""
|
||||||
|
@ -169,4 +165,3 @@ class GpioSensorMcp3008Plugin(GpioSensorPlugin):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import time
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
from platypush.context import get_plugin
|
from platypush.context import get_plugin
|
||||||
from platypush.config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class Direction(enum.Enum):
|
class Direction(enum.Enum):
|
||||||
|
@ -16,6 +15,7 @@ class Direction(enum.Enum):
|
||||||
DIR_AUTO_TOGGLE = 'auto_toggle'
|
DIR_AUTO_TOGGLE = 'auto_toggle'
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyPep8Naming
|
||||||
class GpioZeroborgPlugin(Plugin):
|
class GpioZeroborgPlugin(Plugin):
|
||||||
"""
|
"""
|
||||||
ZeroBorg plugin. It allows you to control a ZeroBorg
|
ZeroBorg plugin. It allows you to control a ZeroBorg
|
||||||
|
@ -28,10 +28,12 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
_direction = None
|
_direction = None
|
||||||
_init_in_progress = threading.Lock()
|
_init_in_progress = threading.Lock()
|
||||||
|
|
||||||
|
def __init__(self, directions=None, **kwargs):
|
||||||
def __init__(self, directions = {}, *args, **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::
|
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
|
:type directions: dict
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if directions is None:
|
||||||
|
directions = {}
|
||||||
|
|
||||||
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
|
import platypush.plugins.gpio.zeroborg.lib as ZeroBorg
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.directions = directions
|
self.directions = directions
|
||||||
self.auto_mode = False
|
self.auto_mode = False
|
||||||
|
@ -90,8 +102,8 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
self.zb.SetCommsFailsafe(True)
|
self.zb.SetCommsFailsafe(True)
|
||||||
self.zb.ResetEpo()
|
self.zb.ResetEpo()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _get_measurement(self, plugin, timeout):
|
def _get_measurement(plugin, timeout):
|
||||||
measure_start_time = time.time()
|
measure_start_time = time.time()
|
||||||
value = None
|
value = None
|
||||||
|
|
||||||
|
@ -123,11 +135,10 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
direction = sensor['below_threshold_direction']
|
direction = sensor['below_threshold_direction']
|
||||||
|
|
||||||
self.logger.info('Sensor: {}\tMeasurement: {}\tDirection: {}'
|
self.logger.info('Sensor: {}\tMeasurement: {}\tDirection: {}'
|
||||||
.format(sensor['plugin'], value, direction))
|
.format(sensor['plugin'], value, direction))
|
||||||
|
|
||||||
return direction
|
return direction
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def drive(self, direction):
|
def drive(self, direction):
|
||||||
"""
|
"""
|
||||||
|
@ -140,17 +151,12 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prev_direction = self._direction
|
|
||||||
|
|
||||||
self._can_run = True
|
self._can_run = True
|
||||||
self._direction = direction.lower()
|
self._direction = direction.lower()
|
||||||
self.logger.info('Received ZeroBorg drive command: {}'.format(direction))
|
self.logger.info('Received ZeroBorg drive command: {}'.format(direction))
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
while self._can_run and self._direction:
|
while self._can_run and self._direction:
|
||||||
left = 0.0
|
|
||||||
right = 0.0
|
|
||||||
|
|
||||||
if self._direction == Direction.DIR_AUTO_TOGGLE.value:
|
if self._direction == Direction.DIR_AUTO_TOGGLE.value:
|
||||||
if self.auto_mode:
|
if self.auto_mode:
|
||||||
self._direction = None
|
self._direction = None
|
||||||
|
@ -182,13 +188,11 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
|
|
||||||
self.auto_mode = False
|
self.auto_mode = False
|
||||||
|
|
||||||
|
|
||||||
self._drive_thread = threading.Thread(target=_run)
|
self._drive_thread = threading.Thread(target=_run)
|
||||||
self._drive_thread.start()
|
self._drive_thread.start()
|
||||||
|
|
||||||
return {'status': 'running', 'direction': direction}
|
return {'status': 'running', 'direction': direction}
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
|
@ -202,8 +206,7 @@ class GpioZeroborgPlugin(Plugin):
|
||||||
self.zb.MotorsOff()
|
self.zb.MotorsOff()
|
||||||
self.zb.ResetEpo()
|
self.zb.ResetEpo()
|
||||||
|
|
||||||
return {'status':'stopped'}
|
return {'status': 'stopped'}
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,11 @@ import serial
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyBroadException
|
||||||
class SerialPlugin(GpioSensorPlugin):
|
class SerialPlugin(GpioSensorPlugin):
|
||||||
"""
|
"""
|
||||||
The serial plugin can read data from a serial device, as long as the serial
|
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.serial_lock = threading.Lock()
|
||||||
self.last_measurement = None
|
self.last_measurement = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def _read_json(self, serial_port):
|
def _read_json(serial_port):
|
||||||
n_brackets = 0
|
n_brackets = 0
|
||||||
is_escaped_ch = False
|
is_escaped_ch = False
|
||||||
parse_start = False
|
parse_start = False
|
||||||
|
@ -122,10 +123,10 @@ class SerialPlugin(GpioSensorPlugin):
|
||||||
serial_available = self.serial_lock.acquire(timeout=2)
|
serial_available = self.serial_lock.acquire(timeout=2)
|
||||||
if serial_available:
|
if serial_available:
|
||||||
try:
|
try:
|
||||||
ser = self._get_serial(device=device)
|
ser = self._get_serial(device=device, baud_rate=baud_rate)
|
||||||
except:
|
except:
|
||||||
time.sleep(1)
|
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)
|
data = self._read_json(ser)
|
||||||
|
|
||||||
|
@ -147,7 +148,6 @@ class SerialPlugin(GpioSensorPlugin):
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def read(self, device=None, baud_rate=None, size=None, end=None):
|
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):
|
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')
|
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')
|
raise RuntimeError('The serial end must be a single character, not a string')
|
||||||
|
|
||||||
data = bytes()
|
data = bytes()
|
||||||
|
@ -188,10 +188,10 @@ class SerialPlugin(GpioSensorPlugin):
|
||||||
serial_available = self.serial_lock.acquire(timeout=2)
|
serial_available = self.serial_lock.acquire(timeout=2)
|
||||||
if serial_available:
|
if serial_available:
|
||||||
try:
|
try:
|
||||||
ser = self._get_serial(device=device)
|
ser = self._get_serial(device=device, baud_rate=baud_rate)
|
||||||
except:
|
except:
|
||||||
time.sleep(1)
|
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:
|
if size is not None:
|
||||||
for _ in range(0, size):
|
for _ in range(0, size):
|
||||||
|
@ -255,10 +255,10 @@ class SerialPlugin(GpioSensorPlugin):
|
||||||
serial_available = self.serial_lock.acquire(timeout=2)
|
serial_available = self.serial_lock.acquire(timeout=2)
|
||||||
if serial_available:
|
if serial_available:
|
||||||
try:
|
try:
|
||||||
ser = self._get_serial(device=device)
|
ser = self._get_serial(device=device, baud_rate=baud_rate)
|
||||||
except:
|
except:
|
||||||
time.sleep(1)
|
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))
|
self.logger.info('Writing {} to {}'.format(data, self.device))
|
||||||
ser.write(data)
|
ser.write(data)
|
||||||
|
@ -270,4 +270,3 @@ class SerialPlugin(GpioSensorPlugin):
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue