forked from platypush/platypush
Added new sensors plugin to webpanel
This commit is contained in:
parent
b932df1c12
commit
1eae45805d
|
@ -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',
|
||||
'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',
|
||||
|
|
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:
|
||||
|
||||
* :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:
|
||||
|
||||
|
|
|
@ -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,16 +135,15 @@ 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():
|
||||
|
@ -152,7 +153,6 @@ class LeapListener(Leap.Listener):
|
|||
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:
|
||||
|
||||
|
|
|
@ -20,4 +20,3 @@ class SensorMcp3008Backend(SensorBackend):
|
|||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -47,4 +47,3 @@ class SensorDataBelowThresholdEvent(Event):
|
|||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -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,7 +28,7 @@ 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()}
|
||||
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import threading
|
||||
import time
|
||||
|
||||
from platypush.plugins import action
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import enum
|
||||
import time
|
||||
|
||||
from platypush.plugins import action
|
||||
from platypush.plugins.gpio.sensor import GpioSensorPlugin
|
||||
|
@ -105,7 +104,6 @@ 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
|
||||
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -127,7 +139,6 @@ class GpioZeroborgPlugin(Plugin):
|
|||
|
||||
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):
|
||||
"""
|
||||
|
@ -206,4 +210,3 @@ class GpioZeroborgPlugin(Plugin):
|
|||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in a new issue