Stairs/web/app.js

1073 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function startApp()
{
// Source: https://github.com/axios/axios/issues/164
axios.interceptors.response.use(undefined, function axiosRetryInterceptor(err) {
var config = err.config;
// If config does not exist or the retry option is not set, reject
if(!config || !config.retry) return Promise.reject(err);
// Set the variable for keeping track of the retry count
config.__retryCount = config.__retryCount || 0;
// Check if we've maxed out the total number of retries
if(config.__retryCount >= config.retry) {
// Reject with the error
return Promise.reject(err);
}
// Increase the retry count
config.__retryCount += 1;
// Create new promise to handle exponential backoff
var backoff = new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, config.retryDelay || 1);
});
// Return the promise in which recalls axios to retry the request
return backoff.then(function() {
return axios(config);
});
});
Vue.component('check', {
template: '<div class="check" :class="{ checked: value, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
props: {
title: String,
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick: function()
{
if (this.disabled) return;
this.value = !this.value;
this.$emit('input', this.value);
},
handleKeyDown: function(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
});
Vue.component('radio', {
template: '<div class="radio" :class="{ checked: value == id, disabled: disabled }" @keydown="handleKeyDown" @click="handleClick" tabindex="0"><div class="control"><div class="inner"></div></div><div class="label">{{ title }}</div></div>',
props: {
title: String,
value: null,
id: null,
disabled: {
type: Boolean,
default: false
}
},
methods: {
handleClick: function()
{
if (this.disabled) return;
this.value = this.id;
this.$emit('input', this.value);
},
handleKeyDown: function(event)
{
if (event.keyCode == 32)
{
this.handleClick();
event.preventDefault();
}
}
}
});
Vue.component('range', {
template: '<div>' +
'<div class="start">' +
'<span class="value">{{ value.start }}</span>' +
'<div class="slidercontainer">' +
'<input type="range" min="0" max="4094" class="slider" v-model.number="value.start">' +
'</div>' +
'</div>' +
'<div class="end">' +
'<span class="value">{{ value.end }}</span>' +
'<div class="slidercontainer">' +
'<input type="range" min="1" max="4095" class="slider" v-model.number="value.end">' +
'</div>' +
'</div>' +
'</div>',
props: ['value'],
mounted: function()
{
this.oldValue = { start: this.value.start, end: this.value.end };
},
watch: {
value: {
handler: function(newValue)
{
if (newValue.start != this.oldValue.start)
{
if (newValue.start > newValue.end)
{
newValue.end = newValue.start + 1;
this.$emit('input', newValue);
}
}
else if (newValue.end != this.oldValue.end)
{
if (newValue.end < newValue.start)
{
newValue.start = newValue.end - 1;
this.$emit('input', newValue);
}
}
this.oldValue.start = newValue.start;
this.oldValue.end = newValue.end;
},
deep: true
}
}
});
var i18n = new VueI18n({
locale: navigator.language.split('-')[0],
fallbackLocale: 'en',
messages: messages
});
var app = new Vue({
el: '#app',
i18n: i18n,
data: {
notification: null,
loading: true,
saving: false,
savingSteps: false,
loadingIndicator: '|',
uploadProgress: false,
savingCalibration: false,
activeTab: 'status',
status: {
systemID: 'loading...',
version: 'loading...',
time: null,
timeOffset: 0,
resetReason: null,
stackTrace: false
},
wifiStatus: {
ap: {
enabled: false,
ip: '0.0.0.0'
},
station: {
enabled: false,
status: 0,
ip: '0.0.0.0'
}
},
searchingLocation: false,
triggers: {
time: {
enabled: false,
transitionTime: null,
triggers: []
},
motion: {
enabled: false,
enabledDuringTimeTrigger: 0,
enabledDuringDay: false,
transitionTime: null,
delay: null,
triggers: []
}
},
connection: {
hostname: null,
accesspoint: true,
station: false,
ssid: null,
password: null,
dhcp: true,
ip: null,
subnetmask: null,
gateway: null
},
system: {
ntpServer: null,
ntpInterval: 5,
lat: null,
lng: null,
pins: {
ledAP: null,
ledSTA: null,
apButton: null,
pwmSDA: null,
pwmSCL: null
},
pwmAddress: null,
pwmFrequency: null,
mapsAPIKey: ''
},
allSteps: true,
allStepsValue: 0,
steps: [],
location: '',
fixedTimes: [],
relativeTimes: [],
calibration: null
},
created: function()
{
var self = this;
self.notificationTimer = null;
document.title = i18n.t('title');
var hash = window.location.hash.substr(1);
if (hash)
self.activeTab = hash;
self.startLoadingIndicator();
self.updateWiFiStatus();
self.disableStepsChanged = false;
self.savingStepsTimer = false;
self.savingCalibrationTimer = false;
var fixedTimes = [];
var relativeTimes = [];
for (var minutes = 0; minutes < 60 * 24; minutes += 15)
fixedTimes.push(minutes);
for (var minutes = -(4 * 60); minutes <= 4 * 60; minutes += 15)
relativeTimes.push(minutes);
self.fixedTimes = fixedTimes;
self.relativeTimes = relativeTimes;
// Sequential loading of all the settings makes sure
// we don't overload the ESP8266 with requests, as that
// can cause it to run out of memory easily.
// This is a horrible way to implement it, but I don't feel like
// including a big library or working out a clean short solution
// at the moment, and it works :)
self.loadStatus().then(function()
{
self.loadConnection().then(function()
{
self.loadSystem().then(function()
{
self.loadTimeTriggers().then(function()
{
self.loadMotionTriggers().then(function()
{
self.loadSteps().then(function()
{
self.stopLoadingIndicator();
self.loading = false;
});
});
});
});
});
});
},
methods: {
showNotification: function(message, error)
{
var self = this;
self.notification = {
message: message,
error: error
};
if (self.notificationTimer != null)
clearTimeout(self.notificationTimer);
self.notificationTimer = setTimeout(function()
{
self.notification = null;
self.notificationTimer = null;
}, 5000);
},
hideNotification: function()
{
var self = this;
self.notification = null;
if (self.notificationTimer != null)
{
clearTimeout(self.notificationTimer);
self.notificationTimer = null;
}
},
handleAPIError: function(messageId, error)
{
var self = this;
console.log(error);
var errorMessage = '';
if (error.response)
{
errorMessage = 'HTTP response code ' + error.response.status;
}
else if (error.request)
{
errorMessage = 'No response';
}
else
{
errorMessage = error.message;
}
self.showNotification(i18n.t(messageId) + '\n\n' + errorMessage, true);
},
loadStatus: function()
{
var self = this;
return axios.get('/api/status', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.status = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadStatus'));
},
loadConnection: function()
{
var self = this;
return axios.get('/api/connection', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.connection = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadConnection'));
},
loadSystem: function()
{
var self = this;
return axios.get('/api/system', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.system = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadSystem'));
},
loadTimeTriggers: function()
{
var self = this;
return axios.get('/api/triggers/time', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
{
var timeSettings = {
enabled: response.data.enabled || false,
transitionTime: response.data.transitionTime || 0,
triggers: []
};
if (Array.isArray(response.data.triggers))
{
for (var i = 0; i < response.data.triggers.length; i++)
{
var trigger = response.data.triggers[i];
timeSettings.triggers.push({
brightness: trigger.brightness || 0,
triggerType: trigger.triggerType || 0,
enabled: trigger.enabled || false,
fixedTime: trigger.triggerType > 0 ? 0 : trigger.time || 0,
relativeTime: trigger.triggerType > 0 ? trigger.time || 0 : 0,
monday: (trigger.daysOfWeek & 1) > 0,
tuesday: (trigger.daysOfWeek & 2) > 0,
wednesday: (trigger.daysOfWeek & 4) > 0,
thursday: (trigger.daysOfWeek & 8) > 0,
friday: (trigger.daysOfWeek & 16) > 0,
saturday: (trigger.daysOfWeek & 32) > 0,
sunday: (trigger.daysOfWeek & 64) > 0
});
}
}
self.triggers.time = timeSettings;
}
})
.catch(self.handleAPIError.bind(self, 'error.loadTimeTriggers'));
},
loadMotionTriggers: function()
{
var self = this;
return axios.get('/api/triggers/motion', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.triggers.motion = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadMotionTriggers'));
},
loadSteps: function()
{
var self = this;
return axios.get('/api/steps/values', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (Array.isArray(response.data))
{
var allSteps = true;
var allStepsValue = false;
var total = 0;
var steps = [];
for (var i = 0; i < response.data.length; i++)
{
var value = response.data[i];
if (allStepsValue === false)
allStepsValue = value;
else if (value !== allStepsValue)
allSteps = false;
steps.push({ value: value });
total += value;
}
self.steps = steps;
self.allStepsValue = Math.floor(total / steps.length);
self.allSteps = allSteps;
}
});
},
applyConnection: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/connection', {
hostname: self.connection.hostname,
accesspoint: self.connection.accesspoint,
station: self.connection.station,
ssid: self.connection.ssid,
password: self.connection.password,
dhcp: self.connection.dhcp,
ip: self.connection.ip,
subnetmask: self.connection.subnetmask,
gateway: self.connection.gateway,
}, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.applyConnection'))
.then(function()
{
self.saving = false;
});
},
applySystem: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/system', self.system, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.applySystem'))
.then(function()
{
self.saving = false;
});
},
startLoadingIndicator: function()
{
var self = this;
self.loadingStage = 0;
self.loadingTimer = setInterval(function()
{
self.loadingStage++;
switch (self.loadingStage)
{
case 1: self.loadingIndicator = '/'; break;
case 2: self.loadingIndicator = '-'; break;
case 3: self.loadingIndicator = '\\'; break;
case 4: self.loadingIndicator = '|'; self.loadingStage = 0; break;
}
}, 250);
},
stopLoadingIndicator: function()
{
clearInterval(this.loadingTimer);
},
getWiFiStationStatus: function()
{
if (!this.wifiStatus.station.enabled)
return 'disconnected';
switch (this.wifiStatus.station.status)
{
case 0: // WL_IDLE_STATUS
case 2: // WL_SCAN_COMPLETED
return 'connecting';
case 1: // WL_NO_SSID_AVAIL
case 4: // WL_CONNECT_FAILED
case 5: // WL_CONNECTION_LOST
return 'error';
case 3: // WL_CONNECTED
return 'connected';
case 6: // WL_DISCONNECTED
default:
return 'disconnected';
}
},
getWiFiStationStatusText: function()
{
if (!this.wifiStatus.station.enabled)
return i18n.t('wifiStatus.stationmode.disabled');
switch (this.wifiStatus.station.status)
{
case 0: // WL_IDLE_STATUS
return i18n.t('wifiStatus.stationmode.idle');
case 1: // WL_NO_SSID_AVAIL
return i18n.t('wifiStatus.stationmode.noSSID');
case 2: // WL_SCAN_COMPLETED
return i18n.t('wifiStatus.stationmode.scanCompleted');
case 3: // WL_CONNECTED
return this.wifiStatus.station.ip;
case 4: // WL_CONNECT_FAILED
return i18n.t('wifiStatus.stationmode.connectFailed');
case 5: // WL_CONNECTION_LOST
return i18n.t('wifiStatus.stationmode.connectionLost');
case 6: // WL_DISCONNECTED
default:
return i18n.t('wifiStatus.stationmode.disconnected');
}
},
updateWiFiStatus: function()
{
var self = this;
if (!self.saving)
{
axios.get('/api/connection/status', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
self.wifiStatus = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.updateWiFiStatus'))
.then(function()
{
setTimeout(self.updateWiFiStatus, 5000);
});
}
else
setTimeout(self.updateWiFiStatus, 5000);
},
uploadFirmware: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
self.uploadProgress = 0;
var data = new FormData();
data.append('file', document.getElementById('firmwareFile').files[0]);
var config = {
timeout: 360000,
onUploadProgress: function(progressEvent)
{
self.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
}
};
axios.post('/api/firmware', data, config)
.then(function(response)
{
self.showNotification(i18n.t('rebootPending'));
})
.catch(self.handleAPIError.bind(self, 'error.uploadFirmware'))
.then(function()
{
self.uploadProgress = false;
self.saving = false;
document.getElementById('firmware').reset();
});
},
stepsChanged: function()
{
var self = this;
if (self.loading || self.disableStepsChanged) return;
if (self.savingStepsTimer === false)
self.savingStepsTimer = setTimeout(function() { self.updateSteps(); }, 200);
},
updateSteps: function()
{
var self = this;
if (self.savingSteps)
self.savingStepsTimer = setTimeout(function() { self.updateSteps(); }, 200);
self.savingSteps = true;
self.savingStepsTimer = false;
self.disableStepsChanged = true;
if (self.allSteps)
{
var newSteps = [];
for (var i = 0; i < self.steps.length; i++)
newSteps.push({ value: self.allStepsValue });
self.steps = newSteps;
}
else
{
var total = 0;
for (var i = 0; i < self.steps.length; i++)
total += self.steps[i].value;
self.allStepsValue = Math.floor(total / self.steps.length);
}
var steps = [];
for (var i = 0; i < self.steps.length; i++)
steps.push(self.steps[i].value);
self.disableStepsChanged = false;
axios.post('/api/steps/values', {
transitionTime: 1000,
values: steps,
retry: 10,
retryDelay: 1000
})
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.updateSteps'))
.then(function()
{
self.savingSteps = false;
});
},
searchLocation: function()
{
var self = this;
if (!self.location) return;
self.searchingLocation = true;
axios.get('https://maps.googleapis.com/maps/api/geocode/json', { params: { address: self.location, key: self.system.mapsAPIKey }})
.then(function(response)
{
if (Array.isArray(response.data.results) && response.data.results.length > 0)
{
var location = response.data.results[0].geometry.location;
self.system.lat = location.lat;
self.system.lng = location.lng;
}
})
.catch(self.handleAPIError.bind(self, 'error.searchLocation'))
.then(function()
{
self.searchingLocation = false;
});
},
applyTimeTriggers: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
var timeSettings = {
enabled: self.triggers.time.enabled,
transitionTime: self.triggers.time.transitionTime,
triggers: []
};
for (var i = 0; i < self.triggers.time.triggers.length; i++)
{
var trigger = self.triggers.time.triggers[i];
timeSettings.triggers.push({
brightness: trigger.brightness,
triggerType: trigger.triggerType,
enabled: trigger.enabled,
time: trigger.triggerType > 0 ? trigger.relativeTime : trigger.fixedTime,
daysOfWeek: (trigger.monday ? 1 : 0) |
(trigger.tuesday ? 2 : 0) |
(trigger.wednesday ? 4 : 0) |
(trigger.thursday ? 8 : 0) |
(trigger.friday ? 16 : 0) |
(trigger.saturday ? 32 : 0) |
(trigger.sunday ? 64 : 0)
});
}
axios.post('/api/triggers/time', timeSettings, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.applyTimeTriggers'))
.then(function()
{
self.saving = false;
});
},
addTimeTrigger: function()
{
var self = this;
self.triggers.time.triggers.push({
brightness: 0,
triggerType: 0,
enabled: true,
fixedTime: 9 * 60,
relativeTime: 0,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: true,
sunday: true
});
},
deleteTimeTrigger: function(index)
{
var self = this;
self.triggers.time.triggers.splice(index, 1);
},
applyMotionTriggers: function()
{
var self = this;
if (self.saving) return;
self.saving = true;
axios.post('/api/triggers/motion', self.triggers.motion, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.applyMotionTriggers'))
.then(function()
{
self.saving = false;
});
},
addMotionTrigger: function()
{
var self = this;
self.triggers.motion.triggers.push({
brightness: 0,
enabled: true,
pin: 2,
direction: 1
});
},
deleteMotionTrigger: function(index)
{
var self = this;
self.triggers.motion.triggers.splice(index, 1);
},
getDisplayTime: function(time, isRelative)
{
var result = '';
if (isRelative)
result += time >= 0 ? '+' : '-';
time = Math.abs(time);
var hours = Math.floor(time / 60);
var minutes = time % 60;
result += hours + ':';
if (minutes < 10)
result += '0';
result += minutes;
return result;
},
startCalibration: function()
{
var self = this;
axios.get('/api/steps', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
if (typeof response.data == 'object')
{
self.calibration = {
wizardStep: 0,
count: response.data.count,
useCurve: response.data.useCurve,
ranges: response.data.ranges
};
}
})
.catch(self.handleAPIError.bind(self, 'error.loadSteps'));
},
stopCalibration: function()
{
this.calibration = null;
},
applyCalibration: function()
{
// All the changes are already applied
this.stopCalibration();
},
hasNextCalibrationStep: function()
{
return this.calibration.wizardStep < 1;
},
nextCalibrationStep: function()
{
var self = this;
if (self.calibration.wizardStep == 0)
{
if (self.calibration.count < 1)
self.calibration.count = 1;
else if (self.calibration.count > 16)
self.calibration.count = 16;
// Update ranges with possible new count
if (self.calibration.ranges.length > self.calibration.count)
{
self.calibration.ranges.splice(self.calibration.count);
}
else
{
while (self.calibration.ranges.length < self.calibration.count)
self.calibration.ranges.push({ start: 0, end: 4095 });
}
}
self.calibration.wizardStep++;
if (self.calibration.wizardStep == 2)
self.applyCalibration();
},
calibrationChanged: function()
{
var self = this;
if (self.loadingCalibration || self.calibration === null) return;
if (self.savingCalibrationTimer === false)
self.savingCalibrationTimer = setTimeout(function() { self.updateCalibration(); }, 200);
},
updateCalibration: function()
{
var self = this;
if (self.calibration === null)
return;
if (self.savingCalibration)
self.savingStepsTimer = setTimeout(function() { self.updateCalibration(); }, 200);
self.savingCalibration = true;
self.savingCalibrationTimer = false;
axios.post('/api/steps', {
count: self.calibration.count,
useCurve: self.calibration.useCurve,
ranges: self.calibration.ranges
}, { retry: 10, retryDelay: 1000 })
.then(function(response)
{
})
.catch(self.handleAPIError.bind(self, 'error.updateCalibration'))
.then(function()
{
self.savingCalibration = false;
});
},
deleteStackTrace: function()
{
var self = this;
return axios.get('/api/stacktrace/delete', { retry: 10, retryDelay: 1000 })
.then(function(response)
{
self.status.resetReason = 0;
self.status.stackTrace = false;
})
.catch(self.handleAPIError.bind(self, 'error.stackTraceDeleteError'));
}
},
computed: {
hasResetError: function()
{
var self = this;
/*
REASON_DEFAULT_RST = 0 normal startup by power on
REASON_WDT_RST = 1 hardware watch dog reset
REASON_EXCEPTION_RST = 2 exception reset, GPIO status wont change
REASON_SOFT_WDT_RST = 3 software watch dog reset, GPIO status wont change
REASON_SOFT_RESTART = 4 software restart ,system_restart , GPIO status wont change
REASON_DEEP_SLEEP_AWAKE = 5 wake up from deep-sleep
REASON_EXT_SYS_RST = 6 system reset
*/
return (self.status.resetReason === 1 ||
self.status.resetReason === 2 ||
self.status.resetReason === 3 ||
self.status.stackTrace);
},
getDeviceTime: function()
{
var self = this;
if (self.status.time === false)
return '';
var date = new Date(self.status.time * 1000);
var hours = date.getHours();
var minutes = '0' + date.getMinutes();
var offsetHours = '0' + Math.floor(Math.abs(self.status.timeOffset / 60) / 60);
var offsetMinutes = '0' + Math.abs(self.status.timeOffset / 60) % 60;
var offset = (self.status.timeOffset >= 0 ? '+' : '-') + offsetHours.substr(-2) + ':' + offsetMinutes.substr(-2);
return hours + ':' + minutes.substr(-2) + ' (' + offset + ')';
}
},
watch: {
// The sync: true ensures we can wrap the internal updates with disableStepsChanged
allSteps: {
handler: function() { this.stepsChanged(); },
sync: true
},
allStepsValue: {
handler: function() { this.stepsChanged(); },
sync: true
},
steps: {
handler: function() { this.stepsChanged(); },
deep: true,
sync: true
},
activeTab: function(newValue)
{
window.location.hash = '#' + newValue;
},
calibration: {
handler: function() { this.calibrationChanged(); },
deep: true
}
}
});
}