Stairs/web/app.js

815 lines
21 KiB
JavaScript

function startApp()
{
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();
}
}
}
});
var i18n = new VueI18n({
locale: navigator.language,
fallbackLocale: 'en',
messages: messages
});
var app = new Vue({
el: '#app',
i18n: i18n,
data: {
notification: null,
loading: true,
saving: false,
savingSteps: false,
loadingIndicator: '|',
uploadProgress: false,
activeTab: 'status',
version: {
systemID: 'loading...',
version: 'loading...',
},
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: 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: []
},
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;
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.loadVersion().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);
},
loadVersion: function()
{
var self = this;
return axios.get('/api/version')
.then(function(response)
{
if (typeof response.data == 'object')
self.version = response.data;
})
.catch(self.handleAPIError.bind(self, 'error.loadVersion'));
},
loadConnection: function()
{
var self = this;
return axios.get('/api/connection')
.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')
.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')
.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')
.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')
.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,
})
.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)
.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')
.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;
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
})
.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)
.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)
.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: 0
});
},
deleteMotionTrigger: function(index)
{
var self = this;
self.triggers.motion.triggers.splice(index, 1);
},
getDisplayTime: function(time, isRelative)
{
var result = '';
if (isRelative && time >= 0)
result += '+';
var hours = Math.floor(time / 60);
var minutes = Math.abs(time) % 60;
result += hours + ':';
if (minutes < 10)
result += '0';
result += minutes;
return result;
}
},
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;
}
}
});
}