Home-AssistantConfig/www/custom_ui/floorplan/lib/fully-kiosk.js

791 lines
26 KiB
JavaScript
Executable File

/*
Floorplan Fully Kiosk for Home Assistant
Version: 1.0.7.50
By Petar Kozul
https://github.com/pkozul/ha-floorplan
*/
'use strict';
(function () {
if (typeof window.FullyKiosk === 'function') {
return;
}
class FullyKiosk {
constructor(floorplan) {
this.version = '1.0.7.50';
this.floorplan = floorplan;
this.authToken = (window.localStorage && window.localStorage.authToken) ? window.localStorage.authToken : '';
this.fullyInfo = {};
this.fullyState = {};
this.beacons = {};
this.throttledFunctions = {};
}
/***************************************************************************************************************************/
/* Initialization
/***************************************************************************************************************************/
init() {
this.logInfo('VERSION', `Fully Kiosk v${this.version}`);
/*
let uuid = 'a445425b-c718-461c-a876-aa647abd99d4';
let deviceId = uuid.replace(/[-_]/g, '').toUpperCase();
let payload = { room: 'entry hall', id: uuid, distance: 123.45 };
this.PostToHomeAssistant(`/api/room_presence/${deviceId}`, payload);
*/
if (typeof fully === "undefined") {
this.logInfo('FULLY_KIOSK', `Fully Kiosk is not running or not enabled. You can enable it via Settings > Other Settings > Enable Website Integration (PLUS).`);
return;
}
let macAddress = fully.getMacAddress().toLowerCase();
let device = this.floorplan.config && this.floorplan.config.fully_kiosk &&
this.floorplan.config.fully_kiosk.find(x => x.address.toLowerCase() == macAddress);
if (!device) {
return;
}
if (!navigator.geolocation) {
this.logInfo('FULLY_KIOSK', "Geolocation is not supported or not enabled. You can enable it via Settings > Web Content Settings > Enable Geolocation Access (PLUS) and on the device via Google Settings > Location > Fully Kiosk Browser.");
}
this.fullyInfo = this.getFullyInfo(device);
this.updateFullyState();
this.updateCurrentPosition();
this.initAudio();
this.addAudioEventHandlers();
this.addFullyEventHandlers();
this.subscribeHomeAssistantEvents();
this.sendMotionState();
this.sendPluggedState();
this.sendScreensaverState();
this.sendMediaPlayerState();
}
initAudio() {
this.audio = new Audio();
this.isAudioPlaying = false;
}
getFullyInfo(device) {
return {
motionBinarySensorEntityId: device.motion_sensor,
pluggedBinarySensorEntityId: device.plugged_sensor,
screensaverLightEntityId: device.screensaver_light,
mediaPlayerEntityId: device.media_player,
locationName: device.presence_detection ? device.presence_detection.location_name : undefined,
startUrl: fully.getStartUrl(),
currentLocale: fully.getCurrentLocale(),
ipAddressv4: fully.getIp4Address(),
ipAddressv6: fully.getIp6Address(),
macAddress: fully.getMacAddress(),
wifiSSID: fully.getWifiSsid(),
serialNumber: fully.getSerialNumber(),
deviceId: fully.getDeviceId(),
isMotionDetected: false,
isScreensaverOn: false,
supportsGeolocation: (navigator.geolocation != undefined),
};
}
updateFullyState() {
this.fullyState.batteryLevel = fully.getBatteryLevel();
this.fullyState.screenBrightness = fully.getScreenBrightness();
this.fullyState.isScreenOn = fully.getScreenOn();
this.fullyState.isPluggedIn = fully.isPlugged();
}
/***************************************************************************************************************************/
/* Set up event handlers
/***************************************************************************************************************************/
addAudioEventHandlers() {
this.audio.addEventListener('play', this.onAudioPlay.bind(this));
this.audio.addEventListener('playing', this.onAudioPlaying.bind(this));
this.audio.addEventListener('pause', this.onAudioPause.bind(this));
this.audio.addEventListener('ended', this.onAudioEnded.bind(this));
this.audio.addEventListener('volumechange', this.onAudioVolumeChange.bind(this));
}
addFullyEventHandlers() {
window['onFullyEvent'] = (e) => { window.dispatchEvent(new Event(e)); }
window['onFullyIBeaconEvent'] = (e, uuid, major, minor, distance) => {
let event = new CustomEvent(e, {
detail: { uuid: uuid, major: major, minor: minor, distance: distance, timestamp: new Date() }
});
window.dispatchEvent(event);
}
window.addEventListener('fully.screenOn', this.onScreenOn.bind(this));
window.addEventListener('fully.screenOff', this.onScreenOff.bind(this));
window.addEventListener('fully.networkDisconnect', this.onNetworkDisconnect.bind(this));
window.addEventListener('fully.networkReconnect', this.onNetworkReconnect.bind(this));
window.addEventListener('fully.internetDisconnect', this.onInternetDisconnect.bind(this));
window.addEventListener('fully.internetReconnect', this.onInternetReconnect.bind(this));
window.addEventListener('fully.unplugged', this.onUnplugged.bind(this));
window.addEventListener('fully.pluggedAC', this.onPluggedAC.bind(this));
window.addEventListener('fully.pluggedUSB', this.onPluggedUSB.bind(this));
window.addEventListener('fully.onScreensaverStart', this.onScreensaverStart.bind(this));
window.addEventListener('fully.onScreensaverStop', this.onScreensaverStop.bind(this));
window.addEventListener('fully.onBatteryLevelChanged', this.onBatteryLevelChanged.bind(this));
window.addEventListener('fully.onMotion', this.onMotion.bind(this));
if (this.fullyInfo.supportsGeolocation) {
window.addEventListener('fully.onMovement', this.onMovement.bind(this));
}
if (this.fullyInfo.locationName) {
this.logInfo('KIOSK', 'Listening for beacon messages');
window.addEventListener('fully.onIBeacon', this.onIBeacon.bind(this));
}
fully.bind('screenOn', 'onFullyEvent("fully.screenOn");')
fully.bind('screenOff', 'onFullyEvent("fully.screenOff");')
fully.bind('networkDisconnect', 'onFullyEvent("fully.networkDisconnect");')
fully.bind('networkReconnect', 'onFullyEvent("fully.networkReconnect");')
fully.bind('internetDisconnect', 'onFullyEvent("fully.internetDisconnect");')
fully.bind('internetReconnect', 'onFullyEvent("fully.internetReconnect");')
fully.bind('unplugged', 'onFullyEvent("fully.unplugged");')
fully.bind('pluggedAC', 'onFullyEvent("fully.pluggedAC");')
fully.bind('pluggedUSB', 'onFullyEvent("fully.pluggedUSB");')
fully.bind('onScreensaverStart', 'onFullyEvent("fully.onScreensaverStart");')
fully.bind('onScreensaverStop', 'onFullyEvent("fully.onScreensaverStop");')
fully.bind('onBatteryLevelChanged', 'onFullyEvent("fully.onBatteryLevelChanged");')
fully.bind('onMotion', 'onFullyEvent("fully.onMotion");') // Max. one per second
fully.bind('onMovement', 'onFullyEvent("fully.onMovement");')
fully.bind('onIBeacon', 'onFullyIBeaconEvent("fully.onIBeacon", "$id1", "$id2", "$id3", $distance);')
}
/***************************************************************************************************************************/
/* Fully Kiosk events
/***************************************************************************************************************************/
onScreenOn() {
this.logDebug('FULLY_KIOSK', 'Screen turned on');
}
onScreenOff() {
this.logDebug('FULLY_KIOSK', 'Screen turned off');
}
onNetworkDisconnect() {
this.logDebug('FULLY_KIOSK', 'Network disconnected');
}
onNetworkReconnect() {
this.logDebug('FULLY_KIOSK', 'Network reconnected');
}
onInternetDisconnect() {
this.logDebug('FULLY_KIOSK', 'Internet disconnected');
}
onInternetReconnect() {
this.logDebug('FULLY_KIOSK', 'Internet reconnected');
}
onUnplugged() {
this.logDebug('FULLY_KIOSK', 'Unplugged AC');
this.fullyState.isPluggedIn = false;
this.sendPluggedState();
}
onPluggedAC() {
this.logDebug('FULLY_KIOSK', 'Plugged AC');
this.fullyState.isPluggedIn = true;
this.sendPluggedState();
}
onPluggedUSB() {
this.logDebug('FULLY_KIOSK', 'Unplugged USB');
this.logDebug('FULLY_KIOSK', 'Device plugged into USB');
}
onScreensaverStart() {
this.fullyState.isScreensaverOn = true;
this.logDebug('FULLY_KIOSK', 'Screensaver started');
this.sendScreensaverState();
}
onScreensaverStop() {
this.fullyState.isScreensaverOn = false;
this.logDebug('FULLY_KIOSK', 'Screensaver stopped');
this.sendScreensaverState();
}
onBatteryLevelChanged() {
this.logDebug('FULLY_KIOSK', 'Battery level changed');
}
onMotion() {
this.fullyState.isMotionDetected = true;
this.logDebug('FULLY_KIOSK', 'Motion detected');
this.sendMotionState();
}
onMovement(e) {
let functionId = 'onMovement';
let throttledFunc = this.throttledFunctions[functionId];
if (!throttledFunc) {
throttledFunc = this.throttle(this.onMovementThrottled.bind(this), 10000);
this.throttledFunctions[functionId] = throttledFunc;
}
return throttledFunc(e);
}
onMovementThrottled() {
this.logDebug('FULLY_KIOSK', 'Movement detected (throttled)');
if (this.fullyInfo.supportsGeolocation) {
this.updateCurrentPosition()
.then(() => {
this.sendMotionState();
});
}
}
onIBeacon(e) {
let functionId = e.detail.uuid;
let throttledFunc = this.throttledFunctions[functionId];
if (!throttledFunc) {
throttledFunc = this.throttle(this.onIBeaconThrottled.bind(this), 10000);
this.throttledFunctions[functionId] = throttledFunc;
}
return throttledFunc(e);
}
onIBeaconThrottled(e) {
let beacon = e.detail;
this.logDebug('FULLY_KIOSK', `Received (throttled) beacon message (${JSON.stringify(beacon)})`);
let beaconId = beacon.uuid;
beaconId += (beacon.major ? `_${beacon.major}` : '');
beaconId += (beacon.minor ? `_${beacon.minor}` : '');
this.beacons[beaconId] = beacon;
this.sendBeaconState(beacon);
}
/***************************************************************************************************************************/
/* HTML5 Audio
/***************************************************************************************************************************/
onAudioPlay() {
this.isAudioPlaying = true;
this.sendMediaPlayerState();
}
onAudioPlaying() {
this.isAudioPlaying = true;
this.sendMediaPlayerState();
}
onAudioPause() {
this.isAudioPlaying = false;
this.sendMediaPlayerState();
}
onAudioEnded() {
this.isAudioPlaying = false;
this.sendMediaPlayerState();
}
onAudioVolumeChange() {
this.sendMediaPlayerState();
}
/***************************************************************************************************************************/
/* Send state to Home Assistant
/***************************************************************************************************************************/
sendMotionState() {
if (!this.fullyInfo.motionBinarySensorEntityId) {
return;
}
clearTimeout(this.sendMotionStateTimer);
let timeout = this.fullyState.isMotionDetected ? 5000 : 10000;
let state = this.fullyState.isMotionDetected ? "on" : "off";
this.PostToHomeAssistant(`/api/states/${this.fullyInfo.motionBinarySensorEntityId}`, this.newPayload(state), () => {
this.sendMotionStateTimer = setTimeout(() => {
this.fullyState.isMotionDetected = false;
this.sendMotionState();
// Send other states as well
this.sendPluggedState();
this.sendScreensaverState();
this.sendMediaPlayerState();
}, timeout);
});
}
sendPluggedState() {
if (!this.fullyInfo.pluggedBinarySensorEntityId) {
return;
}
let state = this.fullyState.isPluggedIn ? "on" : "off";
this.PostToHomeAssistant(`/api/states/${this.fullyInfo.pluggedBinarySensorEntityId}`, this.newPayload(state));
}
sendScreensaverState() {
if (!this.fullyInfo.screensaverLightEntityId) {
return;
}
let state = this.fullyState.isScreensaverOn ? "on" : "off";
this.PostToHomeAssistant(`/api/states/${this.fullyInfo.screensaverLightEntityId}`, this.newPayload(state));
}
sendMediaPlayerState() {
if (!this.fullyInfo.mediaPlayerEntityId) {
return;
}
let state = this.isAudioPlaying ? "playing" : "idle";
this.PostToHomeAssistant(`/api/fully_kiosk/media_player/${this.fullyInfo.mediaPlayerEntityId}`, this.newPayload(state));
}
sendBeaconState(beacon) {
if (!this.fullyInfo.motionBinarySensorEntityId) {
return;
}
/*
let payload = {
name: this.fullyInfo.locationName,
address: this.fullyInfo.macAddress,
device: beacon.uuid,
beaconUUID: beacon.uuid,
latitude: this.position ? this.position.coords.latitude : undefined,
longitude: this.position ? this.position.coords.longitude : undefined,
entry: 1,
}
this.PostToHomeAssistant(`/api/geofency`, payload, undefined, false);
*/
/*
let payload = {
mac: undefined,
dev_id: beacon.uuid.replace(/-/g, '_'),
host_name: undefined,
location_name: this.fullyInfo.macAddress,
gps: this.position ? [this.position.coords.latitude, this.position.coords.longitude] : undefined,
gps_accuracy: undefined,
battery: undefined,
uuid: beacon.uuid,
major: beacon.major,
minor: beacon.minor,
};
this.PostToHomeAssistant(`/api/services/device_tracker/see`, payload);
*/
/*
let fullyId = this.fullyInfo.macAddress.replace(/[:-]/g, "_");
payload = { topic: `room_presence/${fullyId}`, payload: `{ \"id\": \"${beacon.uuid}\", \"distance\": ${beacon.distance} }` };
this.floorplan.hass.callService('mqtt', 'publish', payload);
*/
let deviceId = beacon.uuid.replace(/[-_]/g, '').toUpperCase();
let payload = {
room: this.fullyInfo.locationName,
uuid: beacon.uuid,
major: beacon.major,
minor: beacon.minor,
distance: beacon.distance,
latitude: this.position ? this.position.coords.latitude : undefined,
longitude: this.position ? this.position.coords.longitude : undefined,
};
this.PostToHomeAssistant(`/api/room_presence/${deviceId}`, payload);
}
newPayload(state) {
this.updateFullyState();
let payload = {
state: state,
brightness: this.fullyState.screenBrightness,
attributes: {
volume_level: this.audio.volume,
media_content_id: this.audio.src,
address: this.fullyInfo.macAddress,
mac_address: this.fullyInfo.macAddress,
serial_number: this.fullyInfo.serialNumber,
device_id: this.fullyInfo.deviceId,
battery_level: this.fullyState.batteryLevel,
screen_brightness: this.fullyState.screenBrightness,
_isScreenOn: this.fullyState.isScreenOn,
_isPluggedIn: this.fullyState.isPluggedIn,
_isMotionDetected: this.fullyState.isMotionDetected,
_isScreensaverOn: this.fullyState.isScreensaverOn,
_latitude: this.position && this.position.coords.latitude,
_longitude: this.position && this.position.coords.longitude,
_beacons: JSON.stringify(Object.keys(this.beacons).map(beaconId => this.beacons[beaconId])),
}
};
return payload;
}
/***************************************************************************************************************************/
/* Geolocation
/***************************************************************************************************************************/
setScreenBrightness(brightness) {
fully.setScreenBrightness(brightness);
}
startScreensaver() {
this.logInfo('FULLY_KIOSK', `Starting screensaver`);
fully.startScreensaver();
}
stopScreensaver() {
this.logInfo('FULLY_KIOSK', `Stopping screensaver`);
fully.stopScreensaver();
}
playTextToSpeech(text) {
this.logInfo('FULLY_KIOSK', `Playing text-to-speech: ${text}`);
fully.textToSpeech(text);
}
playMedia(mediaUrl) {
this.audio.src = mediaUrl;
this.logInfo('FULLY_KIOSK', `Playing media: ${this.audio.src}`);
this.audio.play();
}
pauseMedia() {
this.logInfo('FULLY_KIOSK', `Pausing media: ${this.audio.src}`);
this.audio.pause();
}
setVolume(level) {
this.audio.volume = level;
}
PostToHomeAssistant(url, payload, onSuccess) {
let options = {
type: 'POST',
url: url,
headers: { "X-HA-Access": this.authToken },
data: JSON.stringify(payload),
success: function (result) {
this.logDebug('FULLY_KIOSK', `Posted state: ${url} ${JSON.stringify(payload)}`);
if (onSuccess) {
onSuccess();
}
}.bind(this),
error: function (error) {
this.handleError(new URIError(`Error posting state: ${url}`));
}.bind(this)
};
jQuery.ajax(options);
}
subscribeHomeAssistantEvents() {
/*
this.floorplan.hass.connection.subscribeEvents((event) => {
},
'state_changed');
*/
this.floorplan.hass.connection.subscribeEvents((event) => {
if (this.fullyInfo.screensaverLightEntityId && (event.data.domain === 'light')) {
if (event.data.service_data.entity_id.toString() === this.fullyInfo.screensaverLightEntityId) {
switch (event.data.service) {
case 'turn_on':
this.startScreensaver();
break;
case 'turn_off':
this.stopScreensaver();
break;
}
let brightness = event.data.service_data.brightness;
if (brightness) {
this.setScreenBrightness(brightness);
}
}
}
else if (this.fullyInfo.mediaPlayerEntityId && (event.data.domain === 'media_player')) {
let targetEntityId;
let serviceEntityId = event.data.service_data.entity_id;
if (Array.isArray(serviceEntityId)) {
targetEntityId = serviceEntityId.find(entityId => (entityId === this.fullyInfo.mediaPlayerEntityId));
}
else {
targetEntityId = (serviceEntityId === this.fullyInfo.mediaPlayerEntityId) ? serviceEntityId : undefined;
}
if (targetEntityId) {
switch (event.data.service) {
case 'play_media':
this.playMedia(event.data.service_data.media_content_id);
break;
case 'media_play':
this.playMedia();
break;
case 'media_pause':
case 'media_stop':
this.pauseMedia();
break;
case 'volume_set':
this.setVolume(event.data.service_data.volume_level);
break;
default:
this.logWarning('FULLY_KIOSK', `Service not supported: ${event.data.service}`);
break;
}
}
}
/*
if ((event.data.domain === 'tts') && (event.data.service === 'google_say')) {
if (this.fullyInfo.mediaPlayerEntityId === event.data.service_data.entity_id) {
this.logDebug('FULLY_KIOSK', 'Playing TTS using Fully Kiosk');
this.playTextToSpeech(event.data.service_data.message);
}
}
*/
},
'call_service');
}
/***************************************************************************************************************************/
/* Geolocation
/***************************************************************************************************************************/
updateCurrentPosition() {
if (!navigator.geolocation) {
return Promise.resolve(undefined);
}
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
(position) => {
this.logDebug('FULLY_KIOSK', `Current location: latitude: ${position.coords.latitude}, longitude: ${position.coords.longitude}`);
this.position = position;
resolve(position);
},
(err) => {
this.logError('FULLY_KIOSK', 'Unable to retrieve location');
reject(err);
});
})
}
/***************************************************************************************************************************/
/* Errors / logging
/***************************************************************************************************************************/
handleError(message) {
this.floorplan.handleError(message);
}
logError(area, message) {
this.floorplan.logError(message);
}
logWarning(area, message) {
this.floorplan.logWarning(area, message);
}
logInfo(area, message) {
this.floorplan.logInfo(area, message);
}
logDebug(area, message) {
this.floorplan.logDebug(area, message);
}
/***************************************************************************************************************************/
/* Utility functions
/***************************************************************************************************************************/
debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (options) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = setTimeout(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function');
}
if (options) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
return this.debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
})
}
}
window.FullyKiosk = FullyKiosk;
}).call(this);