function startApp() { 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') .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', { 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; } } }); }