UNPKG

pimatic-angular-material-frontend

Version:

Provides an AngularJS webinterface for Pimatic with material design.

1,333 lines (1,184 loc) 64.1 kB
/*! * Name: pimatic-angular-material-frontend * Description: Provides an AngularJS webinterface for Pimatic with material design. * Version: 0.3.3 * Homepage: http://github.com/denniss17/pimatic-angular-material-frontend * Date: 2016-12-27 */ /** * Create the different modules. * The pimaticApp module is the main module. * The pimaticApp.devices module contains device specific controllers or directives. */ angular.module('pimaticApp.configuration', []); angular.module('pimaticApp.devices', []); angular.module('pimaticApp.settings', []); angular.module('pimaticApp.adapters', ['pimaticApp.configuration']); angular.module('pimaticApp.services', ['pimaticApp.adapters', 'pimaticApp.configuration']); /** The main module */ angular.module('pimaticApp', [ 'ngMaterial', 'ngRoute', 'ngMessages', 'pimaticApp.configuration', 'pimaticApp.devices', 'pimaticApp.services', 'pimaticApp.settings', 'pascalprecht.translate', 'mdThemeColors' ]); angular.module('pimaticApp.configuration').provider('config', function () { this.environment = 'production'; this.production = { title: '', version: 'production', pimaticHost: '', adapterName: 'websocketAdapter', debug: false }; this.development = { title: 'Pimatic frontend - DEV', version: 'develop', pimaticHost: '', adapterName: 'fixtureAdapter', debug: true }; this.testing = this.development; this.$get = function () { var config = this.production; switch (this.environment) { case 'testing': config = this.testing; case 'development': config = this.development; } if (config.title == '@@title') { config.title = 'Pimatic'; } return config; } }); angular.module('pimaticApp').config(['$routeProvider', function ($routeProvider) { $routeProvider.when('/home', { templateUrl: 'partials/home.html', controller: 'HomeController' }).when('/loading', { templateUrl: 'partials/loading.html' }).when('/about', { templateUrl: 'partials/about.html' }).when('/login', { templateUrl: 'partials/login.html', controller: 'LoginController' }).when('/home/:pageId', { templateUrl: 'partials/home.html', controller: 'HomeController' }).when('/settings/groups', { templateUrl: 'partials/settings/groups/index.html', controller: 'GroupsController' }).when('/settings/groups/create', { templateUrl: 'partials/settings/groups/create.html', controller: 'GroupsCreateController' }).when('/settings/groups/:id', { templateUrl: 'partials/settings/groups/edit.html', controller: 'GroupsEditController' }).when('/settings/devices', { templateUrl: 'partials/settings/devices/index.html', controller: 'DevicesController' }).otherwise({ redirectTo: '/loading' }); }]); angular.module('pimaticApp').config(['$mdThemingProvider', function ($mdThemingProvider) { $mdThemingProvider.theme('default') .primaryPalette('blue') .accentPalette('orange'); }]); /** * Base API adapter, specifies dummy methods each adapter could override. */ angular.module('pimaticApp.adapters').factory('baseAdapter', [ '$q', function ($q) { return { store: null, toQueryString: function (data, prefix) { var self = this; var strings = []; angular.forEach(data, function (value, key) { var name = angular.isUndefined(prefix) ? encodeURIComponent(key) : prefix + '[' + encodeURIComponent(key) + ']'; strings.push(angular.isObject(value) ? self.toQueryString(value, name) : (name) + '=' + encodeURIComponent(value)); }); return strings.join('&'); }, setStore: function (store) { this.store = store; }, /** * Execute an action for a device. * @param deviceId string The id of the device. * @param actionName string The name of the action to execute. * @param params object Additional parameters of the action. * @return promise A promise. */ deviceAction: function () { return $q(function (resolve, reject) { reject(); }); }, /** * Attempt to login with the given credentials * @param username string The username * @param password string The password * @param rememberMe bool Whether the user should be remembered. Defaults to false. * @returns promise A promise which will be resolved with the user object, or rejected with a message */ login: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); }, /** * Attempt to logout * @returns promise A promise which will be resolved, or rejected with a message */ logout: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); }, /** * Start the provider and reset all caches */ start: function () { }, /** * Load all objects of a certain type. * @param type The type to load the objects of. * @return promise A promise which is resolved when the data is loaded. */ load: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); }, /** * Add a new object. * @param type The type of the object (e.g. 'groups'). * @param object The object to add. * @return promise A promise. When resolved, the final object should be passed as parameter. When rejected, an * error message should be passed as parameter. */ add: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); }, /** * Update an existing object. * @param type The type of the object (e.g. 'groups'). * @param object The object to update. * @return promise A promise. When resolved, the final object should be passed as parameter. When rejected, an * error message should be passed as parameter. */ update: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); }, /** * Remove an existing object. * @param type The type of the object (e.g. 'groups'). * @param object The object to remove. * @return promise A promise. When resolved, the removed should be passed as parameter. When rejected, an * error message should be passed as parameter. */ remove: function () { return $q(function (resolve, reject) { reject('Not implemented'); }); } }; } ]); angular.module('pimaticApp.adapters').factory('fixtureAdapter', [ '$http', '$q', '$rootScope', 'baseAdapter', function ($http, $q, $rootScope, baseAdapter) { var data = {}; var deferedPromises = {}; return angular.extend({}, baseAdapter, { /** * Start the provider and reset all caches */ start: function () { var self = this; this.store.setUser( { 'username': 'admin', 'role': 'admin', 'permissions': { 'pages': 'write', 'rules': 'write', 'variables': 'write', 'messages': 'write', 'events': 'write', 'devices': 'write', 'groups': 'write', 'plugins': 'write', 'updates': 'write', 'controlDevices': true, 'restart': true } } ); // This triggers a redirect $rootScope.setState('done'); // Simulate by loading fixtures $http.get('assets/fixtures/devices.json').then(function (response) { self.addData('devices', response.data); }, function () { self.addData('devices', []); }); $http.get('assets/fixtures/groups.json').then(function (response) { self.addData('groups', response.data); }, function () { self.addData('groups', []); }); $http.get('assets/fixtures/pages.json').then(function (response) { self.addData('pages', response.data); }, function () { self.addData('pages', []); }); $http.get('assets/fixtures/rules.json').then(function (response) { self.addData('rules', response.data); }, function () { self.addData('rules', []); }); $http.get('assets/fixtures/variables.json').then(function (response) { self.addData('variables', response.data); }, function () { self.addData('variables', []); }); }, addData: function (name, objects) { data[name] = objects; this.checkPromises(name); }, // Todo use cache from websocketAdapter checkPromises: function (name) { if (name in deferedPromises) { deferedPromises[name].resolve(data[name]); delete deferedPromises[name]; } }, load: function (name) { if (name in data) { return $q(function (resolve) { resolve(data[name]); }); } else { deferedPromises[name] = $q.defer(); return deferedPromises[name].promise } } }); } ]); angular.module('pimaticApp.adapters').factory('websocketAdapter', [ '$http', '$q', '$rootScope', '$log', 'baseAdapter', 'config', 'toast', function ($http, $q, $rootScope, $log, baseAdapter, config, toast) { /* * Data via this provider comes asynchronously via a websocket, while the data is requested by the application * via the load method. This can lead to 2 situations: * 1. The application requests data, but the data is not there yet. A promise is returned and saved in the cache * When the data is available, the promise is resolved. * 2. The data comes in, but there is no promise to be resolved. The data is temporarily stored in the cache. * When the load() method is called while the data is already in the cache, the returned promise is resolved * immediately and the cache is cleaned. */ var cache = {}; var singulars = { 'groups': 'group' }; return angular.extend({}, baseAdapter, { socket: null, /** * Start the provider and reset all caches */ start: function () { cache = {}; this.setupSocket(); }, /** * Apply changes by executing the given function * @param fn */ apply: function (fn) { // This is a little hack which makes sure that $digest is only called if it is not already running // This is based on the solution found here: // http://stackoverflow.com/questions/14700865/node-js-angularjs-socket-io-connect-state-and-manually-disconnect // // It seems that sometimes after executing a device action (for example by calling GET api/device/dummy/turnOn), // the response of this call comes at the same time (or in the same cycle) as the received updated via the // socket, resulting in errors if you call $apply there. I'm not sure if it also happens in other cases. if ($rootScope.$$phase) { fn(); } else { $rootScope.$apply(fn); } }, setupSocket: function () { var store = this.store; var self = this; if (this.socket !== null) { this.socket.disconnect(); } this.socket = io(config.pimaticHost, { reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 3000, timeout: 20000, forceNew: true }); // Handshaking messages this.socket.on('connect', function () { $log.debug('websocketApi', 'connect'); self.socket.emit('call', { id: 'errorMessageCount', action: 'queryMessagesCount', params: { criteria: { level: 'error' } } }); self.socket.emit('call', { id: 'guiSettings', action: 'getGuiSettings', params: {} }); self.socket.emit('call', { id: 'updateProcessStatus', action: 'getUpdateProcessStatus', params: {} }); }); this.socket.on('error', function (error) { $log.debug('websocketApi', 'error', error); self.apply(function () { // This triggers a redirect $rootScope.setState('unauthenticated'); }); }); this.socket.on('disconnect', function () { $log.debug('websocketApi', 'disconnect'); }); this.socket.on('hello', function (msg) { $log.debug('websocketApi', 'hello', msg); self.apply(function () { self.store.setUser(msg); // This triggers a redirect $rootScope.setState('done'); }); }); // Call result this.socket.on('callResult', function (msg) { $log.debug('websocketApi', 'callResult', msg); switch (msg.id) { case 'errorMessageCount': break; case 'guiSettings': break; case 'updateProcessStatus': break; } }); // Incoming models this.socket.on('devices', function (devices) { $log.debug('websocketApi', 'devices', devices); self.handleIncomingData('devices', devices); }); this.socket.on('rules', function (rules) { $log.debug('websocketApi', 'rules', rules); self.handleIncomingData('rules', rules); }); this.socket.on('variables', function (variables) { $log.debug('websocketApi', 'variables', variables); self.handleIncomingData('variables', variables); }); this.socket.on('pages', function (pages) { $log.debug('websocketApi', 'pages', pages); self.handleIncomingData('pages', pages); }); this.socket.on('groups', function (groups) { $log.debug('websocketApi', 'groups', groups); self.handleIncomingData('groups', groups); }); // Changes this.socket.on('deviceAttributeChanged', function (attrEvent) { $log.debug('websocketApi', 'deviceAttributeChanged', attrEvent); self.apply(function () { var device = store.get('devices', attrEvent.deviceId); if (device !== null) { // Find attribute angular.forEach(device.attributes, function (attribute) { if (attribute.name == attrEvent.attributeName) { attribute.value = attrEvent.value; attribute.lastUpdate = attrEvent.time; } }); } }); }); this.socket.on('variableValueChanged', function (varValEvent) { $log.debug('websocketApi', 'variableValueChanged', varValEvent); self.apply(function () { var v = store.get('variables', varValEvent.variableName); if (v !== null) { v.value = varValEvent.variableValue; } }); }); // Devices this.socket.on('deviceChanged', function (device) { $log.debug('websocketApi', 'deviceChanged', device); self.apply(function () { store.update('devices', device, true); }); }); this.socket.on('deviceRemoved', function (device) { $log.debug('websocketApi', 'deviceRemoved', device); self.apply(function () { store.remove('devices', device, true); }); }); this.socket.on('deviceAdded', function (device) { $log.debug('websocketApi', 'deviceAdded', device); self.apply(function () { store.add('devices', device, true); }); }); this.socket.on('deviceOrderChanged', function (order) { $log.debug('websocketApi', 'deviceOrderChanged', order); }); // Pages this.socket.on('pageChanged', function (page) { $log.debug('websocketApi', 'pageChanged', page); self.apply(function () { store.update('pages', page, true); }); }); this.socket.on('pageRemoved', function (page) { $log.debug('websocketApi', 'pageRemoved', page); self.apply(function () { store.remove('pages', page, true); }); }); this.socket.on('pageAdded', function (page) { $log.debug('websocketApi', 'pageAdded', page); self.apply(function () { store.add('pages', page, true); }); }); this.socket.on('pageOrderChanged', function (order) { $log.debug('websocketApi', 'pageOrderChanged', order); }); // Groups this.socket.on('groupChanged', function (group) { $log.debug('websocketApi', 'groupChanged', group); self.apply(function () { store.update('groups', group, true); }); }); this.socket.on('groupRemoved', function (group) { $log.debug('websocketApi', 'groupRemoved', group); self.apply(function () { store.remove('groups', group, true); }); }); this.socket.on('groupAdded', function (group) { $log.debug('websocketApi', 'groupAdded', group); self.apply(function () { store.add('groups', group, true); }); }); this.socket.on('groupOrderChanged', function (order) { $log.debug('websocketApi', 'groupOrderChanged', order); }); // Rules this.socket.on('ruleChanged', function (rule) { $log.debug('websocketApi', 'ruleChanged', rule); self.apply(function () { store.update('rules', rule, true); }); }); this.socket.on('ruleAdded', function (rule) { $log.debug('websocketApi', 'ruleAdded', rule); self.apply(function () { store.add('rules', rule, true); }); }); this.socket.on('ruleRemoved', function (rule) { $log.debug('websocketApi', 'ruleRemoved', rule); self.apply(function () { store.remove('rules', rule, true); }); }); this.socket.on('ruleOrderChanged', function (order) { $log.debug('websocketApi', 'ruleOrderChanged', order); }); // Variables this.socket.on('variableChanged', function (variable) { $log.debug('websocketApi', 'variableChanged', variable); self.apply(function () { store.update('variables', variable, true); }); }); this.socket.on('variableAdded', function (variable) { $log.debug('websocketApi', 'variableAdded', variable); self.apply(function () { store.add('variables', variable, true); }); }); this.socket.on('variableRemoved', function (variable) { $log.debug('websocketApi', 'variableRemoved', variable); self.apply(function () { store.remove('variables', variable, true); }); }); this.socket.on('variableOrderChanged', function (order) { $log.debug('websocketApi', 'variableOrderChanged', order); }); this.socket.on('updateProcessStatus', function (statusEvent) { $log.debug('websocketApi', 'updateProcessStatus', statusEvent); }); this.socket.on('updateProcessMessage', function (msgEvent) { $log.debug('websocketApi', 'updateProcessMessage', msgEvent); }); this.socket.on('messageLogged', function (entry) { $log.debug('websocketApi', 'messageLogged', entry); if (entry.level != 'debug') { toast.show(entry.msg); } }); }, /** * Attempt to login with the given credentials * @param username string The username * @param password string The password * @param rememberMe bool Whether the user should be remembered. Defaults to false. * @returns promise A promise which will be resolved with the user object, or rejected with a message */ login: function (username, password, rememberMe) { return $q(function (resolve, reject) { var data = { username: username, password: password }; if (rememberMe) { data.rememberMe = true; } $http.post(config.pimaticHost + '/login', data) .success(function (data) { if (data.success) { resolve({ username: data.username, rememberMe: data.rememberMe, role: data.role }); } else { reject(data.message); } }).error(function (data) { reject(data.message); }); }); }, /** * Attempt to logout * @returns promise A promise which will be resolved, or rejected with a message */ logout: function () { return $q(function (resolve) { $http.get(config.pimaticHost + '/logout') .success(function () { resolve(); }).error(function () { // Succesfull logout gives a 401 resolve(); }); }); }, handleIncomingData: function (type, data) { if (type in cache && 'promises' in cache[type]) { // Resolve promises angular.forEach(cache[type].promises, function (deffered) { deffered.resolve(data); }); // Clear cache delete cache[type]; } else { // Cache data cache[type] = {}; cache[type].data = data; } }, deviceAction: function (deviceId, actionName, params) { var self = this; return $q(function (resolve, reject) { var url = config.pimaticHost + '/api/device/' + deviceId + '/' + actionName; if (!angular.isUndefined(params) && angular.isObject(params)) { url += '?' + self.toQueryString(params); } $http.get(url) .success(function (data) { if (data.success) { resolve(); } else { reject(); } }).error(function () { reject(); }); }); }, /** * Add a new object. * @param type The type of the object (e.g. 'groups'). * @param object The object to add. * @return promise A promise which is resolved when the object is added. */ add: function (type, object) { return $q(function (resolve, reject) { var singular = singulars[type]; var data = {}; data[singular] = object; $http.post(config.pimaticHost + '/api/' + type + '/' + object.id, data).then(function (response) { resolve(response[singular]); }, function (response) { reject(response.message); }); }); }, /** * Update an existing object. * @param type The type of the object (e.g. 'groups'). * @param object The object to update. * @return promise A promise. When resolved, the final object should be passed as parameter. When rejected, an * error message should be passed as parameter. */ update: function (type, object) { return $q(function (resolve, reject) { var singular = singulars[type]; var data = {}; data[singular] = object; $http.patch(config.pimaticHost + '/api/' + type + '/' + object.id, data).then(function (response) { resolve(response[singular]); }, function (response) { reject(response.message); }); }); }, /** * Remove an existing object. * @param type The type of the object (e.g. 'groups'). * @param object The object to remove. * @return promise A promise. When resolved, the removed should be passed as parameter. When rejected, an * error message should be passed as parameter. */ remove: function (type, object) { return $q(function (resolve, reject) { $http.delete(config.pimaticHost + '/api/' + type + '/' + object.id).then(function (response) { resolve(response.removed); }, function (response) { reject(response.message); }); }); }, /** * Load all objects of a certain type. * @param type The type to load the objects of. * @return promise promise A promise which is resolved when the data is loaded. */ load: function (type) { var promise; var defered; // Check if the data is cached if (type in cache && 'data' in cache[type]) { promise = $q(function (resolve) { // Resolve immediately resolve(cache[type].data); }); // Clear cache delete cache[type]; // Return the promise return promise; } else { // Data is not cached. We will create a promise and store this promise // Create a promise defered = $q.defer(); // Add the promise if (angular.isUndefined(cache[type])) { cache[type] = {}; } if (angular.isUndefined(cache[type].promises)) { cache[type].promises = []; } cache[type].promises.push(defered); // Return the promise return defered.promise; } } }); } ]); /** * The store is responsible for keeping the references to the different models or requesting them via the specified * adapter. Users can request models from the store. If the models are in the store, the models are directly returned. * If the models are not in the store, the models are requested via the specified adapter */ angular.module('pimaticApp.services').factory('auth', [ 'store', '$injector', '$location', '$q', function (store, $injector, $location, $q) { var auth = { store: store, isLoggedIn: function () { return store.getUser() !== null; }, /** * Attempt to login with the given credentials * @param username string The username * @param password string The password * @param rememberMe bool Whether the user should be remembered. Defaults to false. * @returns promise A promise which will be resolved with the user object, or rejected with a message */ login: function (username, password, rememberMe) { var self = this; return $q(function (resolve, reject) { self.store.adapter.login(username, password, rememberMe).then(function (user) { store.reload(); store.setUser(user); //store.add('user',user); //self.setUser(user, true); resolve(user); }, reject); }); }, logout: function () { var self = this; return $q(function (resolve, reject) { self.store.adapter.logout().then(function () { // Remove user store.setUser(null); // Reset store store.reset(); // Resolve resolve(); }, reject); }); } }; return auth; } ]); angular.module('pimaticApp.services').factory('events', [ 'toast', function (toast) { return { onDeviceActionDone: function (device, action/*, params*/) { toast.show('Succesfully performed "' + action + '" on ' + device.id); }, onDeviceActionFail: function (device, action/*, params*/) { toast.error('Failed to perform "' + action + '" on ' + device.id); } }; } ]); /** * The store is responsible for keeping the references to the different models or requesting them via the specified * Api. Users can request models from the store. If the models are in the store, the models are directly returned. * If the models are not in the store, the models are requested via the specified Api */ angular.module('pimaticApp.services').provider('store', function () { var self = this; this.$get = [ '$q', '$log', '$injector', 'config', function ($q, $log, $injector, config) { self.store.$q = $q; self.store.$log = $log; self.store.adapter = $injector.get(config.adapterName); return self.store; } ]; this.store = { adapter: null, store: {}, /** * Reset the store and retrieve all objects from the API provider again */ reset: function () { this.$log.debug('=== STORE RESET ==='); this.store = { user: {timestamp: 0, loading: false, data: null}, devices: {timestamp: 0, loading: false, data: []}, groups: {timestamp: 0, loading: false, data: []}, pages: {timestamp: 0, loading: false, data: []}, rules: {timestamp: 0, loading: false, data: []}, variables: {timestamp: 0, loading: false, data: []} }; this.adapter.setStore(this); }, reload: function () { this.reset(); this.adapter.start(); }, isLoading: function (type) { return this.store[type].loading; }, getUser: function () { return this.store.user.data; }, setUser: function (user) { this.store.user.data = user; }, /** * Retrieve a list of models or a single model. If the requested models are not yet loaded, either * an empty list or an empty object is returned which is filled when the models are provided by the adapterProvider. * @param type The type of the model to load. * @param id Optional the id of the model to load. If undefined, all instances will be returned. * @param skipApi Optional indicates if the call to the API should be skipped. Defaults to false; * @returns list|object A list of models or a single instance. */ get: function (type, id, skipApi) { var self = this; var item; var date; if (type in self.store) { // Check if data is already fetched if (self.store[type].timestamp === 0 && !self.store[type].loading) { self.store[type].loading = true; // Fetch data via the API if (!skipApi) { self.adapter.load(type).then(function (data) { // Merge the objects self.store[type].data = data; date = new Date(); self.store[type].timestamp = date.getTime(); self.store[type].loading = false; }, function () { // Set to false, so it can be retried self.store[type].loading = false; }); } } if (angular.isUndefined(id)) { // Return all data return self.store[type].data; } else { // Return single item, or null item = null; angular.forEach(self.store[type].data, function (value) { if (value.id == id) { item = value; } }); return item; } } else { // Not valid, return null or empty list return angular.isUndefined(id) ? [] : null; } }, /** * Add a new object of the given type * @param type The type of the object to add * @param object The object to add * @param skipApi bool Optional, whether to skip the call to the api or not. Typical use case for this is when * the addition is originated from the server. Defaults to false */ add: function (type, object, skipApi) { var adapter = this.adapter; var self = this; var add; this.$log.debug('store', 'add()', 'type=', type, 'object=', object, 'skipApi=', skipApi); // Help function // This function is needed because otherwise creating a new object would result in a double addition (first // by calling the API and adding it on success, the by the message passed from the server via the websocket) add = function () { var current = self.get(type, object.id, skipApi); if (current === null) { // Really new return self.$q(function (resolve) { self.get(type, undefined, skipApi).push(object); resolve(object); }); } else { // Not new, update instead return self.update(type, object, skipApi); } }; return self.$q(function (resolve, reject) { if (skipApi) { // Add directly add(object).then(function (result) { resolve(result); }); } else { // Call the API provider adapter.add(type, object).then(function (resultingObject) { // Succesfully added -> add to store add(resultingObject).then(function (result) { resolve(result); }); }, function (message) { // Not added reject(message); }); } }); }, /** * Update an existing object of the given type. The updating can also be done partially: only the attributes present * in the data object are updated, other attributes remain untouched. * @param type The type of the object which is updated * @param object The updated object * @param skipApi bool Optional, whether to skip the call to the api or not. Typical use case for this is when * the addition is originated from the server. Defaults to false */ update: function (type, object, skipApi) { var adapter = this.adapter; var self = this; this.$log.debug('store', 'update()', 'type=', type, 'object=', object, 'skipApi=', skipApi); return self.$q(function (resolve, reject) { var current = self.get(type, object.id); if (current === null) { reject('Fatal: update called, but object does not exist'); return; } if (skipApi) { // Update directly angular.merge(current, object); resolve(current); } else { // Call the API provider adapter.update(type, object).then(function (resultingObject) { // Succesfully updated -> update in store angular.merge(current, resultingObject); resolve(current); }, function (message) { // Not updated reject(message); }); } }); }, /** * Remove an existing object of the given type * @param type The type of the object which is removed * @param object The to be removed object * @param skipApi bool Optional, whether to skip the call to the api or not. Typical use case for this is when * the addition is originated from the server. Defaults to false */ remove: function (type, object, skipApi) { var self = this; var remove; this.$log.debug('store', 'remove()', 'type=', type, 'object=', object, 'skipApi=', skipApi); if (!(type in self.store)) { return self.$q(function (resolve, reject) { reject('Type is not valid'); }); } // Help function remove = function () { // Find index var index = -1; angular.forEach(self.store[type].data, function (value, i) { index = value.id == object.id ? i : index; }); // Remove object if (index >= 0) { self.store[type].data.splice(index, 1); } }; return self.$q(function (resolve, reject) { if (skipApi) { // Update directly remove(object); resolve(object); } else { // Call the API provider self.adapter.remove(type, object).then(function (resultingObject) { // Succesfully removed -> remove in store remove(object); resolve(resultingObject); }, function (message) { // Not removed reject(message); }); } }); } }; }); angular.module('pimaticApp.services').factory('toast', [ '$mdToast', function ($mdToast) { return { show: function (message) { $mdToast.show($mdToast.simple().content(message)); }, error: function (message) { $mdToast.show($mdToast.simple().content(message)); } }; } ]); angular.module('pimaticApp.services').factory('utils', [ 'store', function (store) { return { /** * Get a list of ids of devices which are not in a group * @return array An array containing the ids of the devices which are not in a group */ getUngroupedDeviceIds: function () { var groups = store.get('groups'); var devices = store.get('devices'); var ungrouped = []; // First add all ids angular.forEach(devices, function (value) { ungrouped.push(value.id); }); // Remove ids of devices which are in a group angular.forEach(groups, function (group) { angular.forEach(group.devices, function (deviceId) { var index = ungrouped.indexOf(deviceId); if (index >= 0) { ungrouped.splice(index, 1); } }); }); // Return the result return ungrouped; } }; } ]); angular.module('pimaticApp').filter('elapsed', function () { return function (time) { var hours, output, minutes; hours = Math.floor(time / 3600); output = hours > 9 ? hours : '0' + hours; time -= hours * 3600; minutes = Math.floor(time / 60); output += ':' + (minutes > 9 ? minutes : '0' + minutes); time -= minutes * 60; output += ':' + (time > 9 ? time : '0' + time); return output; }; }); angular.module('pimaticApp').filter('extract', function () { /** * Take an array of objects, extract the value belonging to the given key and return an array containing these values. */ return function (arr, key) { return arr.map(function (value) { return value[key]; }); }; }); angular.module('pimaticApp').filter('intersect', function () { /** * Calculate the intersection of 2 arrays. */ return function (arr1, arr2) { return arr1.filter(function (n) { return arr2.indexOf(n) != -1; }); }; }); angular.module('pimaticApp').controller('ApplicationController', [ '$scope', '$mdSidenav', '$mdMedia', 'auth', 'config', function ($scope, $mdSidenav, $mdMedia, auth, config) { $scope.auth = auth; $scope.config = config; $scope.$mdMedia = $mdMedia; $scope.toggleMenu = function () { $mdSidenav('left').toggle(); }; $scope.logout = function () { $scope.toggleMenu(); auth.logout().then(function () { $scope.setState('unauthenticated'); }); }; }]); angular.module('pimaticApp.devices').controller('ButtonsController', [ '$scope', 'store', 'events', function ($scope, store, events) { $scope.buttonPressed = function (button) { var action = 'buttonPressed'; store.adapter.deviceAction($scope.device.id, action, {'buttonId': button.id}).then(function () { events.onDeviceActionDone($scope.device, action); }, function () { events.onDeviceActionFail($scope.device, action); }); }; } ]); angular.module('pimaticApp.devices').controller('DimmerController', [ '$scope', 'store', 'events', function ($scope, store, events) { $scope.updateDimlevel = function (attribute) { var action = 'changeDimlevelTo'; store.adapter.deviceAction($scope.device.id, action, {'dimlevel': attribute.value}).then(function () { events.onDeviceActionDone($scope.device, action, {'dimlevel': attribute.value}); }, function () { // Reset value events.onDeviceActionFail($scope.device, action, {'dimlevel': attribute.value}); attribute.value = !attribute.value; }); }; } ]); angular.module('pimaticApp.devices').controller('ShutterController', [ '$scope', 'store', 'events', function ($scope, store, events) { $scope.moveUp = function () { var attribute = $scope.getAttribute('position'); var action = attribute.value == 'up' ? 'stop' : 'moveUp'; store.adapter.deviceAction($scope.device.id, action).then(function () { events.onDeviceActionDone($scope.device, action); }, function () { events.onDeviceActionFail($scope.device, action); }); }; $scope.moveDown = function () { var attribute = $scope.getAttribute('position'); var action = attribute.value == 'down' ? 'stop' : 'moveDown'; store.adapter.deviceAction($scope.device.id, action).then(function () { e