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: '
{{ title }}
', 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: '
{{ title }}
', 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: '
' + '
' + '{{ value.start }}' + '
' + '' + '
' + '
' + '
' + '{{ value.end }}' + '
' + '' + '
' + '
' + '
', 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 won’t change REASON_SOFT_WDT_RST = 3 software watch dog reset, GPIO status won’t change REASON_SOFT_RESTART = 4 software restart ,system_restart , GPIO status won’t 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 } } }); }