From c99cd9abbe9e77175686ba8899152f0e414f27fa Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 23 Jun 2018 10:03:32 +0200 Subject: [PATCH 01/33] Fixing telegram bot to be more resistant to errors --- terrariumNotification.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index f57f08c6a..6665daac1 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -172,17 +172,21 @@ def run(self): last_update_id = None while self.__running: - updates = self.__get_updates(last_update_id) - if 'result' in updates and len(updates["result"]) > 0: - last_update_id = max([int(update["update_id"]) for update in updates["result"]]) + 1 - self.__process_messages(updates["result"]) + try: + updates = self.__get_updates(last_update_id) + if 'result' in updates and len(updates["result"]) > 0: + last_update_id = max([int(update["update_id"]) for update in updates["result"]]) + 1 + self.__process_messages(updates["result"]) - elif 'description' in updates: - print updates - print '%s - ERROR - terrariumNotificatio - TelegramBot has issues: %s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],updates['description']) - self.stop() + elif 'description' in updates: + print updates + print '%s - ERROR - terrariumNotificatio - TelegramBot has issues: %s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],updates['description']) + time.sleep(5) - time.sleep(0.5) + time.sleep(0.5) + except Exception, ex: + print ex + time.sleep(5) print '%s - INFO - terrariumNotificatio - TelegramBot is stopped' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],) From dd8cca4baaf6f38547a811e40310c6689ec28405 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 23 Jun 2018 22:38:10 +0200 Subject: [PATCH 02/33] Change quotes --- terrariumNotification.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 6665daac1..13a35a6ad 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -102,7 +102,7 @@ def __get_url(self,url): else: response = requests.get(url) - data = response.content.decode("utf8") + data = response.content.decode('utf8') except Exception, ex: print ex @@ -120,17 +120,17 @@ def __get_json_from_url(self,url): def __get_updates(self,offset=None): self.__last_update_check = int(time.time()) - url = self.__bot_url + "getUpdates?timeout={}".format(terrariumNotificationTelegramBot.__POLL_TIMEOUT) + url = self.__bot_url + 'getUpdates?timeout={}'.format(terrariumNotificationTelegramBot.__POLL_TIMEOUT) if offset: - url += "&offset={}".format(offset) + url += '&offset={}'.format(offset) return self.__get_json_from_url(url) def __process_messages(self,messages): for update in messages: - user = update["message"]["from"]['username'] - text = update["message"]["text"] - chat = int(update["message"]["chat"]["id"]) + user = update['message']['from']['username'] + text = update['message']['text'] + chat = int(update['message']['chat']['id']) if user not in self.__valid_users: self.send_message('Sorry, you are not a valid user for this TerrariumPI.', chat) @@ -151,7 +151,7 @@ def send_message(self,text, chat_id = None): chat_ids = self.__chat_ids if chat_id is None else [int(chat_id)] text = urllib.quote_plus(text) for chat_id in chat_ids: - url = self.__bot_url + "sendMessage?text={}&chat_id={}".format(text, chat_id) + url = self.__bot_url + 'sendMessage?text={}&chat_id={}'.format(text, chat_id) self.__get_url(url) def set_valid_users(self,users = None): @@ -174,9 +174,9 @@ def run(self): while self.__running: try: updates = self.__get_updates(last_update_id) - if 'result' in updates and len(updates["result"]) > 0: - last_update_id = max([int(update["update_id"]) for update in updates["result"]]) + 1 - self.__process_messages(updates["result"]) + if 'result' in updates and len(updates['result']) > 0: + last_update_id = max([int(update['update_id']) for update in updates['result']]) + 1 + self.__process_messages(updates['result']) elif 'description' in updates: print updates @@ -194,7 +194,7 @@ class terrariumNotification(object): __MAX_MESSAGES_TOTAL_PER_MINUTE = 5 __MAX_MESSAGES_PER_MINUTE = 2 - __regex_parse = re.compile(r"%(?P[^% ]+)%") + __regex_parse = re.compile(r'%(?P[^% ]+)%') __default_notifications = { 'environment_light_alarm_low_on' : terrariumNotificationMessage('environment_light_alarm_low_on','Environment light day on','%raw_data%'), @@ -500,7 +500,7 @@ def send_email(self,subject,message): emailMessageAlternative = MIMEMultipart('alternative') emailMessage['From'] = receiver - emailMessage['To'] = re.sub(r"(.*)@(.*)", "\\1+terrariumpi@\\2", receiver, 0, re.MULTILINE) + emailMessage['To'] = re.sub(r'(.*)@(.*)', '\\1+terrariumpi@\\2', receiver, 0, re.MULTILINE) emailMessage['Subject'] = subject emailMessageAlternative.attach(MIMEText(textimage.encode('utf8') + message, 'plain')) @@ -514,7 +514,7 @@ def send_email(self,subject,message): emailMessage.attach(emailMessageRelated) try: - mailserver.sendmail(receiver,re.sub(r"(.*)@(.*)", "\\1+terrariumpi@\\2", receiver, 0, re.MULTILINE),emailMessage.as_string()) + mailserver.sendmail(receiver,re.sub(r'(.*)@(.*)', '\\1+terrariumpi@\\2', receiver, 0, re.MULTILINE),emailMessage.as_string()) except Exception, ex: print ex From 5f4bd5a737f28d0c8ca5e7d4edfa3be69cc7676f Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 23 Jun 2018 22:40:21 +0200 Subject: [PATCH 03/33] Updated data collector: - Removed duplicate data records for power switches and doors - Added and changed indexes for faster quering - Put more logic in queries and less in code This will improve the overall query time with 50%. And improve the average query times with 400%!! --- static/js/terrariumpi.js | 16 +- terrariumCollector.py | 349 ++++++++++++++++++++++----------------- terrariumEngine.py | 18 +- 3 files changed, 205 insertions(+), 178 deletions(-) diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index 7b96c14ef..7f96b6ec4 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1272,21 +1272,23 @@ function history_graph(name, data, type) { if (type == 'switch') { var usage = ''; if (data.totals !== undefined) { - if (data.totals.power_wattage.duration > 0) { - usage = '{{_('Duration')}}: ' + moment.duration(data.totals.power_wattage.duration * 1000).humanize() + if (data.totals.duration > 0) { + usage = '{{_('Duration')}}: ' + moment.duration(data.totals.duration * 1000).humanize() } - if (data.totals.power_wattage.wattage > 0) { - usage += (usage != '' ? ' - ' : '') + '{{_('Total power in kWh')}}: ' + formatNumber(data.totals.power_wattage.wattage / (3600 * 1000)); + if (data.totals.power_wattage > 0) { + usage += (usage != '' ? ' - ' : '') + '{{_('Total power in kWh')}}: ' + formatNumber(data.totals.power_wattage / (3600 * 1000)); } - if (data.totals.water_flow.water > 0) { - usage += (usage != '' ? ' - ' : '') + '{{_('Total water in L')}}: ' + formatNumber(data.totals.water_flow.water); + if (data.totals.water_flow > 0) { + usage += (usage != '' ? ' - ' : '') + '{{_('Total water in L')}}: ' + formatNumber(data.totals.water_flow); } } $('#' + name + ' .total_usage').text(usage); } else if (type == 'door') { var usage = ''; if (data.totals !== undefined) { - usage = '{{_('Total open for')}}: ' + moment.duration(data.totals.duration * 1000).humanize(); + if (data.totals.duration > 0) { + usage = '{{_('Total open for')}}: ' + moment.duration(data.totals.duration * 1000).humanize(); + } } $('#' + name + ' .total_usage').text(usage); } diff --git a/terrariumCollector.py b/terrariumCollector.py index 413b4700e..b0a4dd6ee 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -27,8 +27,8 @@ def __connect(self): logger.info('Database connection created to database %s' % (terrariumCollector.DATABASE,)) def __create_database_structure(self): - with self.db: - cur = self.db.cursor() + with self.db as db: + cur = db.cursor() cur.execute('''CREATE TABLE IF NOT EXISTS sensor_data (id VARCHAR(50), type VARCHAR(15), @@ -38,34 +38,32 @@ def __create_database_structure(self): limit_max FLOAT(4), alarm_min FLOAT(4), alarm_max FLOAT(4), - alarm INTEGER(1) )''') + alarm INTEGER(1))''') cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS sensor_data_unique ON sensor_data(id,type,timestamp ASC)') cur.execute('CREATE INDEX IF NOT EXISTS sensor_data_timestamp ON sensor_data(timestamp ASC)') - cur.execute('CREATE INDEX IF NOT EXISTS sensor_data_type ON sensor_data(type)') - cur.execute('CREATE INDEX IF NOT EXISTS sensor_data_id ON sensor_data(id)') + cur.execute('CREATE INDEX IF NOT EXISTS sensor_data_avg ON sensor_data(type,timestamp ASC)') + cur.execute('CREATE INDEX IF NOT EXISTS sensor_data_id ON sensor_data(id,timestamp ASC)') cur.execute('''CREATE TABLE IF NOT EXISTS switch_data (id VARCHAR(50), timestamp INTEGER(4), state INTERGER(1), power_wattage FLOAT(2), - water_flow FLOAT(2) - )''') + water_flow FLOAT(2))''') cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS switch_data_unique ON switch_data(id,timestamp ASC)') cur.execute('CREATE INDEX IF NOT EXISTS switch_data_timestamp ON switch_data(timestamp ASC)') - cur.execute('CREATE INDEX IF NOT EXISTS switch_data_id ON switch_data(id)') + cur.execute('CREATE INDEX IF NOT EXISTS switch_data_id ON switch_data(id,timestamp ASC)') cur.execute('''CREATE TABLE IF NOT EXISTS door_data (id INTEGER(4), timestamp INTEGER(4), - state TEXT CHECK( state IN ('open','closed') ) NOT NULL DEFAULT 'closed' - )''') + state TEXT CHECK( state IN ('open','closed') ) NOT NULL DEFAULT 'closed')''') cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS door_data_unique ON door_data(id,timestamp ASC)') cur.execute('CREATE INDEX IF NOT EXISTS door_data_timestamp ON door_data(timestamp ASC)') - cur.execute('CREATE INDEX IF NOT EXISTS door_data_id ON door_data(id)') + cur.execute('CREATE INDEX IF NOT EXISTS door_data_id ON door_data(id,timestamp ASC)') cur.execute('''CREATE TABLE IF NOT EXISTS weather_data (timestamp INTEGER(4), @@ -74,8 +72,7 @@ def __create_database_structure(self): pressure FLOAT(4), wind_direction VARCHAR(50), weather VARCHAR(50), - icon VARCHAR(50) - )''') + icon VARCHAR(50))''') cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS weather_data_unique ON weather_data(timestamp ASC)') @@ -93,22 +90,30 @@ def __create_database_structure(self): memory_free INTEGER(6), disk_total INTEGER(6), disk_used INTEGER(6), - disk_free INTEGER(6) - )''') + disk_free INTEGER(6))''') cur.execute('CREATE UNIQUE INDEX IF NOT EXISTS system_data_unique ON system_data(timestamp ASC)') - self.db.commit() + db.commit() def __upgrade(self,to_version): # Set minimal version to 3.0.0 current_version = 300 table_upgrades = {'310' : ['ALTER TABLE system_data ADD COLUMN disk_total INTEGER(6)', 'ALTER TABLE system_data ADD COLUMN disk_used INTEGER(6)', - 'ALTER TABLE system_data ADD COLUMN disk_free INTEGER(6)']} - - with self.db: - cur = self.db.cursor() + 'ALTER TABLE system_data ADD COLUMN disk_free INTEGER(6)'], + + '380' : ['DROP INDEX IF EXISTS sensor_data_type', + 'CREATE INDEX IF NOT EXISTS sensor_data_avg ON sensor_data (type, timestamp ASC)', + 'DROP INDEX IF EXISTS sensor_data_id', + 'CREATE INDEX IF NOT EXISTS sensor_data_id ON sensor_data (id, timestamp ASC)', + 'DROP INDEX IF EXISTS switch_data_id', + 'CREATE INDEX IF NOT EXISTS switch_data_id ON switch_data (id, timestamp ASC)', + 'DROP INDEX IF EXISTS door_data_id', + 'CREATE INDEX IF NOT EXISTS door_data_id ON door_data (id, timestamp ASC)']} + + with self.db as db: + cur = db.cursor() db_version = int(cur.execute('PRAGMA user_version').fetchall()[0][0]) if db_version > current_version: current_version = db_version @@ -118,8 +123,8 @@ def __upgrade(self,to_version): elif current_version < to_version: logger.info('Collector database is out of date. Running updates from %s to %s' % (current_version,to_version)) # Execute updates - with self.db: - cur = self.db.cursor() + with self.db as db: + cur = db.cursor() for update_version in table_upgrades.keys(): if current_version < int(update_version) <= to_version: # Execute all updates between the versions @@ -131,12 +136,49 @@ def __upgrade(self,to_version): if 'duplicate column name' not in str(ex): logger.error('Error updating collector database. Please contact support. Error message: %s' % (ex,)) - logger.info('Cleaning up disk space. This will take a couple of minutes depending on the database size and sd card disk speed.') - cur.execute('VACUUM') + if '380' == update_version: + self.__upgrade_to_380() + + if int(update_version) % 10 == 0: + logger.info('Cleaning up disk space. This will take a couple of minutes depending on the database size and sd card disk speed.') + cur.execute('VACUUM') + cur.execute('PRAGMA user_version = ' + str(to_version)) logger.info('Updated collector database. Set version to: %s' % (to_version,)) - self.db.commit() + db.commit() + + def __upgrade_to_380(self): + # This update will remove 'duplicate' records that where added for better graphing... This will now be done at the collecting the data + tables = ['door_data','switch_data'] + + with self.db as db: + for table in tables: + cur = db.cursor() + data = cur.execute('SELECT id, timestamp, state FROM ' + table + ' ORDER BY id ASC, timestamp ASC') + data = data.fetchall() + + prev_state = None + prev_id = None + for row in data: + if prev_id is None: + prev_id = row['id'] + + elif prev_id != row['id']: + prev_id = row['id'] + prev_state = None + + if prev_state is None: + prev_state = row['state'] + continue + + if row['state'] == prev_state: + cur.execute('DELETE FROM ' + table + ' WHERE id = ? AND timestamp = ? AND state = ?', (row['id'],row['timestamp'],row['state'])) + + prev_state = row['state'] + prev_id = row['id'] + + db.commit() def __recover(self): starttime = time.time() @@ -174,6 +216,8 @@ def __recover(self): logger.warn('TerrariumPI Collecter recovery mode is finished in %s seconds!', (time.time()-starttime,)) def __log_data(self,type,id,newdata): + timer = time.time() + if self.__recovery: logger.warn('TerrariumPI Collecter is in recovery mode. Cannot store new logging data!') return @@ -184,8 +228,8 @@ def __log_data(self,type,id,newdata): now -= (now % terrariumCollector.STORE_MODULO) try: - with self.db: - cur = self.db.cursor() + with self.db as db: + cur = db.cursor() if type in ['humidity','moisture','temperature','distance','ph','conductivity','light']: cur.execute('REPLACE INTO sensor_data (id, type, timestamp, current, limit_min, limit_max, alarm_min, alarm_max, alarm) VALUES (?,?,?,?,?,?,?,?,?)', @@ -203,108 +247,52 @@ def __log_data(self,type,id,newdata): if 'time' in newdata: now = newdata['time'] - # Make a duplicate of last state and save it with 1 sec back in time to smooth the graphs - cur.execute('''REPLACE INTO switch_data (id,timestamp,state,power_wattage,water_flow) - SELECT id, ? as curtimestamp,state,power_wattage,water_flow - FROM switch_data - WHERE id = ? ORDER BY timestamp DESC LIMIT 1''', (now-1, id)) - cur.execute('REPLACE INTO switch_data (id, timestamp, state, power_wattage, water_flow) VALUES (?,?,?,?,?)', (id, now, newdata['state'], newdata['power_wattage'], newdata['water_flow'])) if type in ['door']: - # Make a duplicate of last state and save it with 1 sec back in time to smooth the graphs - cur.execute('''REPLACE INTO door_data (id,timestamp,state) - SELECT id, ? as curtimestamp,state - FROM door_data - WHERE id = ? ORDER BY timestamp DESC LIMIT 1''', (now-1, id)) - cur.execute('REPLACE INTO door_data (id, timestamp, state) VALUES (?,?,?)', (id, now, newdata)) - self.db.commit() + db.commit() except sqlite3.DatabaseError as ex: logger.error('TerrariumPI Collecter exception! %s', (ex,)) if 'database disk image is malformed' == str(ex): self.__recover() - def __calculate_power_and_water_usage(self,history): - if 'switches' not in history: - return - - now = int(time.time()) * 1000 - for switchid in history['switches']: - # First add a new element to all the data arrays with the current timestamp. This is needed for: - # - Better power usage calculation - # - Better graphs in the interface - history['switches'][switchid]['power_wattage'].append([now,history['switches'][switchid]['power_wattage'][-1][1]]) - history['switches'][switchid]['water_flow'].append([now,history['switches'][switchid]['water_flow'][-1][1]]) - history['switches'][switchid]['state'].append([now,history['switches'][switchid]['state'][-1][1]]) - - totals = {'power_wattage' : {'duration' : 0.0 , 'wattage' : 0.0}, - 'water_flow' : {'duration' : 0.0 , 'water' : 0.0}} - power_on_time = None - for counter,state in enumerate(history['switches'][switchid]['state']): - if state[1] > 0 and power_on_time is None: # Power went on! The value could be variable from zero to 100. Above zero is 'on' - power_on_time = counter - elif power_on_time is not None: # Now check if the power went off, or put on a second time... - power_wattage_start = history['switches'][switchid]['power_wattage'][power_on_time][1] * (history['switches'][switchid]['state'][power_on_time][1] / 100.0) - power_wattage_end = history['switches'][switchid]['power_wattage'][counter][1] * (state[1] / 100.0) - power_wattage = (power_wattage_start + power_wattage_end) / 2.0 - - water_flow_start = history['switches'][switchid]['water_flow'][power_on_time][1] * (history['switches'][switchid]['state'][power_on_time][1] / 100.0) - water_flow_end = history['switches'][switchid]['water_flow'][counter][1] * (state[1] / 100.0) - water_flow = (water_flow_start + water_flow_end) / 2.0 - - duration = (state[0] - history['switches'][switchid]['state'][power_on_time][0]) / 1000.0 # Devide by 1000 because history is using Javascript timestamps - - totals['power_wattage']['duration'] += duration - totals['power_wattage']['wattage'] += (duration * power_wattage) - - totals['water_flow']['duration'] += duration - totals['water_flow']['water'] += (duration * (water_flow / 60)) # Water flow is in Liter per minute. So devide by 60 to get per seconds - - if state[1] == 0: - power_on_time = None # Power went down. Reset so we can measure new period - else: - power_on_time = counter # Change in power useage (dimmer) - - # Here we change the wattage and water flow to zero if the switch was off. This is needed for drawing the right graphs - if state[1] == 0: - history['switches'][switchid]['power_wattage'][counter][1] = 0 - history['switches'][switchid]['water_flow'][counter][1] = 0 - else: - history['switches'][switchid]['power_wattage'][counter][1] *= (state[1] / 100.0) - history['switches'][switchid]['water_flow'][counter][1] *= (state[1] / 100.0) - - history['switches'][switchid]['totals'] = totals - - def __calculate_door_usage(self,history): - if 'doors' not in history: - return - - now = int(time.time()) * 1000 - for doorid in history['doors']: - history['doors'][doorid]['state'].append([now,history['doors'][doorid]['state'][-1][1]]) - - totals = {'duration': 0} - door_open_on_time = None - for counter,state in enumerate(history['doors'][doorid]['state']): - if state[1] != 'closed' and door_open_on_time is None: # Door went open! - door_open_on_time = counter - elif state[1] == 'closed' and door_open_on_time is not None: # Door is closed. Calc period and data - totals['duration'] += (state[0] - history['doors'][doorid]['state'][door_open_on_time][0]) / 1000.0 # Devide by 1000 because history is using Javascript timestamps - door_open_on_time = None # Reset so we can measure new period - - # Here we translate closed to zero and open to one. Else the graphs will not work - history['doors'][doorid]['state'][counter][1] = (0 if state[1] == 'closed' else 1) - - history['doors'][doorid]['totals'] = totals + logger.debug('Timing: updating %s data in %s seconds.' % (type,time.time()-timer)) def stop(self): self.db.close() logger.info('Shutdown data collector') + def get_total_power_water_usage(self): + timer = time.time() + + totals = {'power_wattage' : {'duration' : int(time.time()) , 'wattage' : 0.0}, + 'water_flow' : {'duration' : int(time.time()) , 'water' : 0.0}} + + sql = '''SELECT SUM(total_wattage) AS Watt, SUM(total_water) AS Water, SUM(duration_in_seconds) AS TotalTime FROM ( + SELECT + t1.timestamp-t2.timestamp AS duration_in_seconds, + (t1.timestamp-t2.timestamp) * (t2.state / 100.0) * t2.power_wattage AS total_wattage, + ((t1.timestamp-t2.timestamp) / 60.0) * (t2.state / 100.0) * t2.water_flow AS total_water + FROM switch_data AS t1 + LEFT JOIN switch_data AS t2 + ON t2.id = t1.id + AND t2.timestamp = (SELECT MAX(timestamp) FROM switch_data WHERE timestamp < t1.timestamp AND id = t1.id) + WHERE t2.state > 0)''' + + with self.db as db: + cur = db.cursor() + cur.execute(sql) + row = cur.fetchone() + totals = {'power_wattage' : {'duration' : int(row['TotalTime']) , 'wattage' : float(row['Watt'])}, + 'water_flow' : {'duration' : int(row['TotalTime']) , 'water' : float(row['Water'])}} + + logger.debug('Timing: Total power and water usage calculation done in %s seconds.' % ((time.time() - timer),)) + return totals + def log_switch_data(self,data): if data['hardwaretype'] not in ['pwm-dimmer','remote-dimmer']: # Store normal switches with value 100 indicating full power (aka no dimming) @@ -381,15 +369,40 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): filters = (stoptime,starttime,parameters[0],) elif logtype == 'switches': - fields = { 'power_wattage' : [], 'water_flow' : [] , 'state' : []} - sql = 'SELECT id, "switches" as type, timestamp, ' + ', '.join(fields.keys()) + ' FROM switch_data WHERE timestamp >= ? and timestamp <= ? ' + fields = { 'power_wattage' : [], 'water_flow' : [] } + sql = '''SELECT id, "switches" as type, timestamp, timestamp2, state, ''' + ', '.join(fields.keys()) + ''' FROM ( + SELECT + t2.id AS id, + t2.timestamp AS timestamp, + t1.timestamp AS timestamp2, + (t2.state / 100.0) * t2.power_wattage AS power_wattage, + (t2.state / 100.0) * t2.water_flow AS water_flow, + t2.state AS state + FROM switch_data AS t1 + LEFT JOIN switch_data AS t2 + ON t2.id = t1.id + AND t2.timestamp = (SELECT MAX(timestamp) FROM switch_data WHERE timestamp < t1.timestamp AND id = t1.id) ) + WHERE id IS NOT NULL + AND timestamp >= ? AND timestamp <= ?''' + if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' filters = (stoptime,starttime,parameters[0],) elif logtype == 'doors': - fields = { 'state' : []} - sql = 'SELECT id, "doors" as type, timestamp, ' + ', '.join(fields.keys()) + ' FROM door_data WHERE timestamp >= ? and timestamp <= ? ' + fields = {'state' : []} + sql = '''SELECT id, "doors" as type, timestamp, timestamp2, (CASE WHEN state == 'open' THEN 1 ELSE 0 END) AS state FROM ( + SELECT + t2.id AS id, + t2.timestamp AS timestamp, + t1.timestamp AS timestamp2, + t2.state AS state + FROM door_data AS t1 + LEFT JOIN door_data AS t2 + ON t2.id = t1.id + AND t2.timestamp = (SELECT MAX(timestamp) FROM door_data WHERE timestamp < t1.timestamp AND id = t1.id) ) + WHERE id IS NOT NULL + AND timestamp >= ? AND timestamp <= ?''' if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' @@ -418,55 +431,83 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): sql = 'SELECT "system" as type, timestamp, ' + ', '.join(fields) + ' FROM system_data WHERE timestamp >= ? and timestamp <= ?' - sql = sql + ' ORDER BY timestamp ASC' + sql = sql + ' ORDER BY timestamp ASC, type ASC' + (', id ASC' if logtype != 'system' else '') - rows = [] if not self.__recovery: try: - with self.db: - cur = self.db.cursor() - cur.execute(sql, filters) - rows = cur.fetchall() - logger.debug('TerrariumPI Collecter history query: %s seconds, %s records -> %s, %s' % (time.time()-timer,len(rows),sql,filters)) + with self.db as db: + cur = db.cursor() + for row in cur.execute(sql, filters): + #if logtype == 'switches' and len(row) == len(fields)+1: + # for field in fields: + # history[field] = row[field] + + # return history + + if row['type'] not in history: + history[row['type']] = {} + + if logtype == 'system': + for field in fields: + system_parts = field.split('_') + if system_parts[0] not in history[row['type']]: + history[row['type']][system_parts[0]] = {} if len(system_parts) == 2 else [] + + if len(system_parts) == 2: + if system_parts[1] not in history[row['type']][system_parts[0]]: + history[row['type']][system_parts[0]][system_parts[1]] = [] + + history[row['type']][system_parts[0]][system_parts[1]].append([row['timestamp'] * 1000,row[field]]) + else: + history[row['type']][system_parts[0]].append([row['timestamp'] * 1000,row[field]]) + + else: + if row['id'] not in history[row['type']]: + history[row['type']][row['id']] = copy.deepcopy(fields) + + if row['type'] in ['switches','doors']: + history[row['type']][row['id']]['totals'] = {'duration' : 0, 'power_wattage' : 0, 'water_flow' : 0} + + if row['type'] in ['switches','doors'] and row['state'] > 0: + # Update totals data + history[row['type']][row['id']]['totals']['duration'] += (row['timestamp2'] - row['timestamp']) + if 'switches' == row['type']: + history[row['type']][row['id']]['totals']['power_wattage'] += (row['timestamp2'] - row['timestamp']) * row['power_wattage'] + history[row['type']][row['id']]['totals']['water_flow'] += (row['timestamp2'] - row['timestamp']) * row['water_flow'] + + for field in fields: + history[row['type']][row['id']][field].append([row['timestamp'] * 1000,row[field]]) + + if row['type'] in ['switches','doors']: + # Add extra point for nicer graphing of doors and power switches + history[row['type']][row['id']][field].append([row['timestamp2'] * 1000,row[field]]) + + logger.debug('Timing: history %s query: %s seconds' % (logtype,time.time()-timer)) except sqlite3.DatabaseError as ex: logger.error('TerrariumPI Collecter exception! %s', (ex,)) if 'database disk image is malformed' == str(ex): self.__recover() - for row in rows: - if logtype == 'switches' and len(row) == len(fields)+1: - for field in fields: - history[field] = row[field] - - return history - - if row['type'] not in history: - history[row['type']] = {} - - if logtype == 'system': - for field in fields: - system_parts = field.split('_') - if system_parts[0] not in history[row['type']]: - history[row['type']][system_parts[0]] = {} if len(system_parts) == 2 else [] - - if len(system_parts) == 2: - if system_parts[1] not in history[row['type']][system_parts[0]]: - history[row['type']][system_parts[0]][system_parts[1]] = [] + # In order to get nicer graphs, we are adding a start and end time based on the selected time range if needed + if logtype in ['switches','doors']: + if logtype in history: + for data_id in history[logtype]: + for field in fields: + first_item = history[logtype][data_id][field][0] + last_item = history[logtype][data_id][field][len(history[logtype][data_id][field])-1] - history[row['type']][system_parts[0]][system_parts[1]].append([row['timestamp'] * 1000,row[field]]) - else: - history[row['type']][system_parts[0]].append([row['timestamp'] * 1000,row[field]]) + if first_item[0] > stoptime: + history[logtype][data_id][field].insert(0,[stoptime * 1000 ,first_item[1]]) - else: - if row['id'] not in history[row['type']]: - history[row['type']][row['id']] = copy.deepcopy(fields) + if last_item[0] < starttime: + history[logtype][data_id][field].append([starttime * 1000 ,last_item[1]]) + elif len(parameters) > 0: + # Create 'empty' history array if single id is requested + history[logtype] = {} + history[logtype][parameters[0]] = copy.deepcopy(fields) for field in fields: - history[row['type']][row['id']][field].append([row['timestamp'] * 1000,row[field]]) - - if logtype == 'switches': - self.__calculate_power_and_water_usage(history) - elif logtype == 'doors': - self.__calculate_door_usage(history) + history[logtype][parameters[0]][field].append([stoptime * 1000,0]) + history[logtype][parameters[0]][field].append([starttime * 1000,0]) return history diff --git a/terrariumEngine.py b/terrariumEngine.py index 7cf7b9790..d8d35ce90 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -370,23 +370,7 @@ def __get_current_power_usage_water_flow(self, socket = False): return data def __get_total_power_usage_water_flow(self): - totals = {'power_wattage' : {'duration' : int(time.time()) , 'wattage' : 0.0}, - 'water_flow' : {'duration' : int(time.time()) , 'water' : 0.0}} - - history = self.collector.get_history(['switches'],int(time.time()),0) - - if 'switches' not in history: - return totals - - for switchid in history['switches']: - totals['power_wattage']['wattage'] += history['switches'][switchid]['totals']['power_wattage']['wattage'] - totals['water_flow']['water'] += history['switches'][switchid]['totals']['water_flow']['water'] - - if history['switches'][switchid]['power_wattage'][0][0] / 1000.0 < totals['power_wattage']['duration']: - totals['power_wattage']['duration'] = history['switches'][switchid]['power_wattage'][0][0] / 1000.0 - - if history['switches'][switchid]['water_flow'][0][0] / 1000.0 < totals['water_flow']['duration']: - totals['water_flow']['duration'] = history['switches'][switchid]['water_flow'][0][0] / 1000.0 + totals = self.collector.get_total_power_water_usage() totals['power_wattage']['duration'] = max(self.get_uptime()['uptime'],int(time.time()) - totals['power_wattage']['duration'],int(time.time()) - totals['water_flow']['duration']) totals['water_flow']['duration'] = totals['power_wattage']['duration'] From 806e48583ea7b2aba396801f41e5e1d95000f3b0 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 23 Jun 2018 22:45:13 +0200 Subject: [PATCH 04/33] Update version number --- defaults.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defaults.cfg b/defaults.cfg index 570aab719..065597147 100644 --- a/defaults.cfg +++ b/defaults.cfg @@ -1,7 +1,7 @@ [terrariumpi] host = :: port = 8090 -version = 3.7.0 +version = 3.8.0 title = TerrariumPI %(version)s power_usage = 5 owfs_port = 4304 From 00b8a6f85096144a2bb2355121f8c2d6046250f1 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 11:34:22 +0200 Subject: [PATCH 05/33] Better and safer upgrade --- terrariumCollector.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terrariumCollector.py b/terrariumCollector.py index b0a4dd6ee..f979b38c1 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -139,6 +139,7 @@ def __upgrade(self,to_version): if '380' == update_version: self.__upgrade_to_380() + db.commit() if int(update_version) % 10 == 0: logger.info('Cleaning up disk space. This will take a couple of minutes depending on the database size and sd card disk speed.') cur.execute('VACUUM') @@ -179,6 +180,7 @@ def __upgrade_to_380(self): prev_id = row['id'] db.commit() + logger.info('Collector database upgrade for version 3.8.0 succeeded! Removed duplicate records') def __recover(self): starttime = time.time() From ccdfb2e2e2ab772afb6f736f9ba778433e750297 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 11:34:33 +0200 Subject: [PATCH 06/33] Fix total power usage --- static/js/terrariumpi.js | 4 +++- terrariumEngine.py | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index 7f96b6ec4..c68037c35 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1511,10 +1511,12 @@ function update_dashboard_uptime(data) { } function update_dashboard_power_usage(data) { + console.log(data); + update_dashboard_tile('power_wattage', formatNumber(data.current) + '/' + formatNumber(data.max)); $("#power_wattage .progress-bar-success").css('height', (data.max > 0 ? (data.current / data.max) * 100 : 0) + '%'); - update_dashboard_tile('total_power',formatNumber(data.total / (3600 * 1000))); // from total watt to KiloWattHours + update_dashboard_tile('total_power',formatNumber(data.total / (3600 / 1000))); // from total watt to KiloWattHours $("#total_power .count_bottom .costs").text(formatCurrency(data.price,2,3)); $("#total_power .count_bottom .duration").text(moment.duration(data.duration * 1000).humanize()); } diff --git a/terrariumEngine.py b/terrariumEngine.py index d8d35ce90..0ed88b42a 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -371,10 +371,6 @@ def __get_current_power_usage_water_flow(self, socket = False): def __get_total_power_usage_water_flow(self): totals = self.collector.get_total_power_water_usage() - - totals['power_wattage']['duration'] = max(self.get_uptime()['uptime'],int(time.time()) - totals['power_wattage']['duration'],int(time.time()) - totals['water_flow']['duration']) - totals['water_flow']['duration'] = totals['power_wattage']['duration'] - totals['power_wattage']['wattage'] += totals['power_wattage']['duration'] * self.pi_power_wattage return totals @@ -905,7 +901,7 @@ def get_power_usage_water_flow(self, socket = False): data['power']['total'] = totaldata['power_wattage']['wattage'] data['power']['duration'] = totaldata['power_wattage']['duration'] - data['power']['price'] = self.config.get_power_price() * (totaldata['power_wattage']['wattage'] / (3600.0 * 1000.0)) + data['power']['price'] = self.config.get_power_price() * (totaldata['power_wattage']['wattage'] / (3600.0 / 1000.0)) data['water']['total'] = totaldata['water_flow']['water'] data['water']['duration'] = totaldata['water_flow']['duration'] From 352626bd6dca81537f516db1fd31072ede6d4f7d Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 11:37:15 +0200 Subject: [PATCH 07/33] Fix total power usage (2) --- static/js/terrariumpi.js | 2 +- terrariumEngine.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index c68037c35..5a8207c1b 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1516,7 +1516,7 @@ function update_dashboard_power_usage(data) { update_dashboard_tile('power_wattage', formatNumber(data.current) + '/' + formatNumber(data.max)); $("#power_wattage .progress-bar-success").css('height', (data.max > 0 ? (data.current / data.max) * 100 : 0) + '%'); - update_dashboard_tile('total_power',formatNumber(data.total / (3600 / 1000))); // from total watt to KiloWattHours + update_dashboard_tile('total_power',formatNumber(data.total / 3600 / 1000)); // from total watt to KiloWattHours $("#total_power .count_bottom .costs").text(formatCurrency(data.price,2,3)); $("#total_power .count_bottom .duration").text(moment.duration(data.duration * 1000).humanize()); } diff --git a/terrariumEngine.py b/terrariumEngine.py index 0ed88b42a..cbef54e02 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -901,7 +901,7 @@ def get_power_usage_water_flow(self, socket = False): data['power']['total'] = totaldata['power_wattage']['wattage'] data['power']['duration'] = totaldata['power_wattage']['duration'] - data['power']['price'] = self.config.get_power_price() * (totaldata['power_wattage']['wattage'] / (3600.0 / 1000.0)) + data['power']['price'] = self.config.get_power_price() * (totaldata['power_wattage']['wattage'] / 3600.0 / 1000.0) data['water']['total'] = totaldata['water_flow']['water'] data['water']['duration'] = totaldata['water_flow']['duration'] From 955052b13715cbd7b28a9d0ebfd027f5db9d5d04 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 15:38:47 +0200 Subject: [PATCH 08/33] Fix telegram bot socks setting #161 --- terrariumNotification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 13a35a6ad..8de5349e5 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -144,7 +144,7 @@ def __process_messages(self,messages): def get_config(self): return {'bot_token' : self.__bot_token, 'userid': ','.join(self.__valid_users) if self.__valid_users is not None else '', - 'proxy' : self.__proxy} + 'proxy' : self.__proxy['https'] if self.__proxy is not None else None} def send_message(self,text, chat_id = None): if self.__running: From a94ca13b0974ec72d9e1606f4a0b1484abe7c9f4 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 15:39:30 +0200 Subject: [PATCH 09/33] Another attempt to get the powerswitches and door nicer graphs --- terrariumCollector.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/terrariumCollector.py b/terrariumCollector.py index f979b38c1..a5aed709b 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -385,11 +385,11 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): ON t2.id = t1.id AND t2.timestamp = (SELECT MAX(timestamp) FROM switch_data WHERE timestamp < t1.timestamp AND id = t1.id) ) WHERE id IS NOT NULL - AND timestamp >= ? AND timestamp <= ?''' + AND timestamp > ? AND timestamp <= ?''' if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' - filters = (stoptime,starttime,parameters[0],) + filters = (stoptime - (24 * 60 * 60),starttime,parameters[0],) elif logtype == 'doors': fields = {'state' : []} @@ -404,11 +404,11 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): ON t2.id = t1.id AND t2.timestamp = (SELECT MAX(timestamp) FROM door_data WHERE timestamp < t1.timestamp AND id = t1.id) ) WHERE id IS NOT NULL - AND timestamp >= ? AND timestamp <= ?''' + AND timestamp > ? AND timestamp <= ?''' if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' - filters = (stoptime,starttime,parameters[0],) + filters = (stoptime - (24 * 60 * 60),starttime,parameters[0],) elif logtype == 'weather': fields = { 'wind_speed' : [], 'temperature' : [], 'pressure' : [] , 'wind_direction' : [], 'rain' : [], @@ -440,6 +440,10 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): with self.db as db: cur = db.cursor() for row in cur.execute(sql, filters): + if row['timestamp'] < stoptime: + continue + + #if logtype == 'switches' and len(row) == len(fields)+1: # for field in fields: # history[field] = row[field] @@ -495,12 +499,7 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): if logtype in history: for data_id in history[logtype]: for field in fields: - first_item = history[logtype][data_id][field][0] last_item = history[logtype][data_id][field][len(history[logtype][data_id][field])-1] - - if first_item[0] > stoptime: - history[logtype][data_id][field].insert(0,[stoptime * 1000 ,first_item[1]]) - if last_item[0] < starttime: history[logtype][data_id][field].append([starttime * 1000 ,last_item[1]]) From 90546b98e95ea8b778d585a97d6b1cc433d1c8a1 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 15:47:29 +0200 Subject: [PATCH 10/33] Update Telegram box proxy settings --- terrariumNotification.py | 1 + 1 file changed, 1 insertion(+) diff --git a/terrariumNotification.py b/terrariumNotification.py index 8de5349e5..396a8b142 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -580,6 +580,7 @@ def set_telegram(self,bot_token,userid,proxy): self.telegram = terrariumNotificationTelegramBot(bot_token,userid,proxy) else: self.telegram.set_valid_users(userid) + self.telegram.set_proxy(proxy) def send_telegram(self,subject,message): if self.telegram is None: From 47a8dc68d4e527d465e18de34d55800279e03c13 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 22:25:57 +0200 Subject: [PATCH 11/33] Stash --- terrariumCollector.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/terrariumCollector.py b/terrariumCollector.py index a5aed709b..4550e2a28 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -437,13 +437,22 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): if not self.__recovery: try: + first_item = None with self.db as db: cur = db.cursor() for row in cur.execute(sql, filters): - if row['timestamp'] < stoptime: + + if row['type'] in ['switches','doors'] and row['timestamp'] < stoptime: + first_item = {} + for field in fields: + first_item[field] = row[field] + + + continue + #if logtype == 'switches' and len(row) == len(fields)+1: # for field in fields: # history[field] = row[field] @@ -499,8 +508,15 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): if logtype in history: for data_id in history[logtype]: for field in fields: + #first_item = history[logtype][data_id][field][0] last_item = history[logtype][data_id][field][len(history[logtype][data_id][field])-1] - if last_item[0] < starttime: + + #if first_item is not None: + # history[logtype][data_id][field].insert(0,[stoptime * 1000 ,first_item[field]]) + + #print 'Logtype: %s, lastitem timestamp %s, < graph last timestampt %s' % (logtype,last_item[0]/1000,starttime) + + if (last_item[0] / 1000) < starttime: history[logtype][data_id][field].append([starttime * 1000 ,last_item[1]]) elif len(parameters) > 0: From ed32f879bddb266d31006c28f7875f32423fe959 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 24 Jun 2018 22:27:19 +0200 Subject: [PATCH 12/33] Small update to installer and reload message settings after saving. #101 #161 --- install.sh | 6 ++++-- terrariumNotification.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index b941bd329..28b39eedd 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,8 @@ if ! hash whiptail 2>/dev/null; then aptitude -y install whiptail fi +clear + whiptail --backtitle "TerrariumPI v. ${VERSION}" --title " TerrariumPI Installer " --yesno "TerrariumPI is going to be installed to run with user '${SCRIPT_USER}'. If this is not the right user stop the installation now!\n\nDo you want to continue?" 0 60 case $? in @@ -104,7 +106,7 @@ EOF if [ ! -d Adafruit_Python_DHT ] then - git clone https://github.com/adafruit/Adafruit_Python_DHT.git >/dev/null + git clone https://github.com/adafruit/Adafruit_Python_DHT.git 2>/dev/null fi @@ -116,7 +118,7 @@ Install required software\n\nUpdating Adafruit DHT python library ... XXX EOF cd Adafruit_Python_DHT -git pull > /dev/null +git pull > /dev/null PROGRESS=$((PROGRESS + 2)) diff --git a/terrariumNotification.py b/terrariumNotification.py index 396a8b142..c3019e90d 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -676,6 +676,7 @@ def set_config(self,data): self.__data.write(configfile) self.__load_config() + self.__load_messages() return True def get_config(self): From bae1084f5479a628cc2761048395b919fd3118c9 Mon Sep 17 00:00:00 2001 From: theyosh Date: Mon, 25 Jun 2018 21:11:26 +0200 Subject: [PATCH 13/33] Final collector code. And good looking graphs --- terrariumCollector.py | 63 ++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/terrariumCollector.py b/terrariumCollector.py index 4550e2a28..cbd284ce6 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -276,14 +276,14 @@ def get_total_power_water_usage(self): sql = '''SELECT SUM(total_wattage) AS Watt, SUM(total_water) AS Water, SUM(duration_in_seconds) AS TotalTime FROM ( SELECT - t1.timestamp-t2.timestamp AS duration_in_seconds, - (t1.timestamp-t2.timestamp) * (t2.state / 100.0) * t2.power_wattage AS total_wattage, - ((t1.timestamp-t2.timestamp) / 60.0) * (t2.state / 100.0) * t2.water_flow AS total_water + t2.timestamp-t1.timestamp AS duration_in_seconds, + (t2.timestamp-t1.timestamp) * (t1.state / 100.0) * t1.power_wattage AS total_wattage, + ((t2.timestamp-t1.timestamp) / 60.0) * (t1.state / 100.0) * t1.water_flow AS total_water FROM switch_data AS t1 LEFT JOIN switch_data AS t2 ON t2.id = t1.id - AND t2.timestamp = (SELECT MAX(timestamp) FROM switch_data WHERE timestamp < t1.timestamp AND id = t1.id) - WHERE t2.state > 0)''' + AND t2.timestamp = (SELECT MIN(timestamp) FROM switch_data WHERE timestamp > t1.timestamp AND id = t1.id) + WHERE t1.state > 0)''' with self.db as db: cur = db.cursor() @@ -374,18 +374,17 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): fields = { 'power_wattage' : [], 'water_flow' : [] } sql = '''SELECT id, "switches" as type, timestamp, timestamp2, state, ''' + ', '.join(fields.keys()) + ''' FROM ( SELECT - t2.id AS id, - t2.timestamp AS timestamp, - t1.timestamp AS timestamp2, - (t2.state / 100.0) * t2.power_wattage AS power_wattage, - (t2.state / 100.0) * t2.water_flow AS water_flow, - t2.state AS state + t1.id AS id, + t1.timestamp AS timestamp, + t2.timestamp AS timestamp2, + (t1.state / 100.0) * t1.power_wattage AS power_wattage, + (t1.state / 100.0) * t1.water_flow AS water_flow, + t1.state AS state FROM switch_data AS t1 LEFT JOIN switch_data AS t2 ON t2.id = t1.id - AND t2.timestamp = (SELECT MAX(timestamp) FROM switch_data WHERE timestamp < t1.timestamp AND id = t1.id) ) - WHERE id IS NOT NULL - AND timestamp > ? AND timestamp <= ?''' + AND t2.timestamp = (SELECT MIN(timestamp) FROM switch_data WHERE timestamp > t1.timestamp AND id = t1.id) ) + WHERE timestamp > ? AND timestamp <= ?''' if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' @@ -395,16 +394,15 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): fields = {'state' : []} sql = '''SELECT id, "doors" as type, timestamp, timestamp2, (CASE WHEN state == 'open' THEN 1 ELSE 0 END) AS state FROM ( SELECT - t2.id AS id, - t2.timestamp AS timestamp, - t1.timestamp AS timestamp2, - t2.state AS state + t1.id AS id, + t1.timestamp AS timestamp, + t2.timestamp AS timestamp2, + t1.state AS state FROM door_data AS t1 LEFT JOIN door_data AS t2 ON t2.id = t1.id - AND t2.timestamp = (SELECT MAX(timestamp) FROM door_data WHERE timestamp < t1.timestamp AND id = t1.id) ) - WHERE id IS NOT NULL - AND timestamp > ? AND timestamp <= ?''' + AND t2.timestamp = (SELECT MIN(timestamp) FROM door_data WHERE timestamp > t1.timestamp AND id = t1.id) ) + WHERE timestamp > ? AND timestamp <= ?''' if len(parameters) > 0 and parameters[0] is not None: sql = sql + ' and id = ?' @@ -442,13 +440,7 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): cur = db.cursor() for row in cur.execute(sql, filters): - if row['type'] in ['switches','doors'] and row['timestamp'] < stoptime: - first_item = {} - for field in fields: - first_item[field] = row[field] - - - + if row['type'] in ['switches','doors'] and row['timestamp2'] is not None and '' != row['timestamp2'] and row['timestamp2'] < stoptime: continue @@ -483,9 +475,10 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): if row['type'] in ['switches','doors']: history[row['type']][row['id']]['totals'] = {'duration' : 0, 'power_wattage' : 0, 'water_flow' : 0} - if row['type'] in ['switches','doors'] and row['state'] > 0: + if row['type'] in ['switches','doors'] and row['state'] > 0 and row['timestamp2'] is not None and '' != row['timestamp2']: # Update totals data history[row['type']][row['id']]['totals']['duration'] += (row['timestamp2'] - row['timestamp']) + if 'switches' == row['type']: history[row['type']][row['id']]['totals']['power_wattage'] += (row['timestamp2'] - row['timestamp']) * row['power_wattage'] history[row['type']][row['id']]['totals']['water_flow'] += (row['timestamp2'] - row['timestamp']) * row['water_flow'] @@ -493,7 +486,7 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): for field in fields: history[row['type']][row['id']][field].append([row['timestamp'] * 1000,row[field]]) - if row['type'] in ['switches','doors']: + if row['type'] in ['switches','doors'] and row['timestamp2'] is not None and '' != row['timestamp2']: # Add extra point for nicer graphing of doors and power switches history[row['type']][row['id']][field].append([row['timestamp2'] * 1000,row[field]]) @@ -508,14 +501,10 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): if logtype in history: for data_id in history[logtype]: for field in fields: - #first_item = history[logtype][data_id][field][0] - last_item = history[logtype][data_id][field][len(history[logtype][data_id][field])-1] - - #if first_item is not None: - # history[logtype][data_id][field].insert(0,[stoptime * 1000 ,first_item[field]]) - - #print 'Logtype: %s, lastitem timestamp %s, < graph last timestampt %s' % (logtype,last_item[0]/1000,starttime) + # For each field, shift the first timestamp to the start of the query time + history[logtype][data_id][field][0][0] = stoptime * 1000 + last_item = history[logtype][data_id][field][len(history[logtype][data_id][field])-1] if (last_item[0] / 1000) < starttime: history[logtype][data_id][field].append([starttime * 1000 ,last_item[1]]) From 33f2a57f30ae2d1dacd89d8c12a17d9c88026631 Mon Sep 17 00:00:00 2001 From: theyosh Date: Mon, 25 Jun 2018 21:20:01 +0200 Subject: [PATCH 14/33] Remove debig --- static/js/terrariumpi.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index 5a8207c1b..c5b783e4b 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1511,8 +1511,6 @@ function update_dashboard_uptime(data) { } function update_dashboard_power_usage(data) { - console.log(data); - update_dashboard_tile('power_wattage', formatNumber(data.current) + '/' + formatNumber(data.max)); $("#power_wattage .progress-bar-success").css('height', (data.max > 0 ? (data.current / data.max) * 100 : 0) + '%'); From e7543c6c5343598caabf8f2950acfc047a45c5df Mon Sep 17 00:00:00 2001 From: theyosh Date: Mon, 25 Jun 2018 21:41:41 +0200 Subject: [PATCH 15/33] Fix tooltip HTML code --- static/js/terrariumpi.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index c5b783e4b..8c27e71b8 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -2609,10 +2609,6 @@ $(document).ready(function() { $(this).on('click', load_page).attr('title',$(this).parents('li').find('a:first').text()); }); - $("
   
").css({ - position: "absolute", - }).appendTo("body"); - load_door_history(); load_player_status(); load_page('dashboard.html'); From e0bf5e67ec68c28c1030d2f4d938ee6884f723c0 Mon Sep 17 00:00:00 2001 From: theyosh Date: Mon, 25 Jun 2018 22:51:04 +0200 Subject: [PATCH 16/33] Better fix for tooltips in graphs --- static/css/terrariumpi.css | 4 ++++ static/js/terrariumpi.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/static/css/terrariumpi.css b/static/css/terrariumpi.css index df27279e6..cea97c3b3 100644 --- a/static/css/terrariumpi.css +++ b/static/css/terrariumpi.css @@ -185,6 +185,10 @@ iframe.external_calendar { #tooltip span { cursor: default; } + +#tooltipGraph { + cursor: default; +} .history_graph { min-height: 175px; display: block !important; diff --git a/static/js/terrariumpi.js b/static/js/terrariumpi.js index 8c27e71b8..68db3a84f 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1294,8 +1294,8 @@ function history_graph(name, data, type) { } $('#' + name + ' .history_graph').bind('plothover', function (event, pos, item) { if (item) { - $('#tooltip').css({top: item.pageY-5, left: item.pageX-5}); - $('#tooltip span').attr('data-original-title',moment(item.datapoint[0]).format('LLL') + '
' + item.series.label + ' ' + item.series.yaxis.tickFormatter(item.datapoint[1],item.series.yaxis)); + $('#tooltipGraph').attr('data-original-title',moment(item.datapoint[0]).format('LLL') + '
' + item.series.label + ' ' + item.series.yaxis.tickFormatter(item.datapoint[1],item.series.yaxis)); + $('#tooltipGraph').css({top: item.pageY-5, left: item.pageX-5, display:'block'}); } }); } @@ -2609,6 +2609,12 @@ $(document).ready(function() { $(this).on('click', load_page).attr('title',$(this).parents('li').find('a:first').text()); }); + $("
   
").css({ + position: "absolute", + }).appendTo("body").tooltip({html:true}).on('hidden.bs.tooltip',function(event){ + $('#tooltipGraph').hide(); + }) + load_door_history(); load_player_status(); load_page('dashboard.html'); From 9879af75238d89ee8f05c636c29fdd1f7f51d42f Mon Sep 17 00:00:00 2001 From: theyosh Date: Tue, 26 Jun 2018 22:06:20 +0200 Subject: [PATCH 17/33] Fix environment status for manual power switch toggling. --- terrariumEnvironment.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/terrariumEnvironment.py b/terrariumEnvironment.py index 0bb5a9953..5a4789708 100644 --- a/terrariumEnvironment.py +++ b/terrariumEnvironment.py @@ -47,25 +47,25 @@ def __get_power_state(self,powerswitchlist): self.timer_min_data['power_state'] = not self.timer_min_data['min_power'] self.timer_max_data['power_state'] = not self.timer_max_data['min_power'] - def __toggle_powerswitches(self,switches,action = None): - for powerswitch in switches: + def __toggle_powerswitches(self,powerswitches,action = None): + for switchid in powerswitches: if 'on' == action: - if 'dimmer' in powerswitch.get_hardware_type() and 'light' != self.get_type(): - powerswitch.go_up() + if 'dimmer' in powerswitches[switchid].get_hardware_type(): + powerswitches[switchid].go_up() else: - powerswitch.on() + powerswitches[switchid].on() elif 'off' == action: - if 'dimmer' in powerswitch.get_hardware_type() and 'light' != self.get_type(): - powerswitch.go_down() + if 'dimmer' in powerswitches[switchid].get_hardware_type(): + powerswitches[switchid].go_down() else: - powerswitch.off() + powerswitches[switchid].off() - self.__get_power_state(switches) + self.__get_power_state(powerswitches) def __toggle_alarm(self,part,action,powerswitchlist,timer = False): now = int(time.time()) - powerswitches = {} + switches = {} lastaction = 0 settletime = 0 onduration = 0 @@ -85,7 +85,10 @@ def __toggle_alarm(self,part,action,powerswitchlist,timer = False): return if now - lastaction > settletime or timer: - self.__toggle_powerswitches([powerswitchlist[switchid] for switchid in powerswitches if switchid in powerswitchlist],action) + for powerswitch in powerswitches: + switches[powerswitch] = powerswitchlist[powerswitch] + + self.__toggle_powerswitches(switches,action) if 'min' == part: self.timer_min_data['lastaction'] = now @@ -537,6 +540,7 @@ def __init__(self, sensors, powerswitches, weather, door_status, config, notific self.sensors = sensors self.powerswitches = powerswitches + self.weather = weather self.load_environment() @@ -690,6 +694,9 @@ def update(self, trigger = True): logger.debug('Environment %s is enabled and based on: %s.' % (environment_part.get_type(), environment_part.get_mode())) environment_part.update(self.sensors,self.powerswitches,self.weather,self.__environment_parts['light']) + if not trigger: + continue + toggle_on_alarm_min = None toggle_on_alarm_max = None @@ -718,9 +725,6 @@ def update(self, trigger = True): logger.debug('Environment %s is has alarm_min: %s, alarm_max: %s, trigger?: %s' % (environment_part.get_type(),toggle_on_alarm_min,toggle_on_alarm_max,trigger)) - if not trigger: - return - if toggle_on_alarm_min is not None and not environment_part.has_alarm_min_powerswitches(): logger.debug('Environment %s alarm min is triggered to state %s, but has no powerswitches configured' % (environment_part.get_type(),toggle_on_alarm_min)) elif toggle_on_alarm_min is not None: From 94a96ccb0daa9c4a5d80ca870c2c61a1663755f5 Mon Sep 17 00:00:00 2001 From: theyosh Date: Tue, 26 Jun 2018 22:32:04 +0200 Subject: [PATCH 18/33] Update translations --- locales/en_US/LC_MESSAGES/terrariumpi.mo | Bin 74577 -> 74041 bytes locales/en_US/LC_MESSAGES/terrariumpi.po | 152 +++++++++++++---------- locales/terrariumpi.pot | 148 ++++++++++++---------- terrariumTranslations.py | 19 +-- terrariumWebserver.py | 4 +- views/notifications.tpl | 4 +- 6 files changed, 180 insertions(+), 147 deletions(-) diff --git a/locales/en_US/LC_MESSAGES/terrariumpi.mo b/locales/en_US/LC_MESSAGES/terrariumpi.mo index 9c39dac26602b66df83215c00bfd6d1cd9a96f34..6bf93e102dc60b6fe2e27f4024d238a75c688e27 100644 GIT binary patch delta 15384 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 delta 16015 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 diff --git a/locales/en_US/LC_MESSAGES/terrariumpi.po b/locales/en_US/LC_MESSAGES/terrariumpi.po index c7e3651dd..aaabbbef3 100644 --- a/locales/en_US/LC_MESSAGES/terrariumpi.po +++ b/locales/en_US/LC_MESSAGES/terrariumpi.po @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: TerrariumPI 3.6.0\n" -"POT-Creation-Date: 2018-06-15 21:59+CEST\n" -"PO-Revision-Date: 2018-06-15 21:59+0200\n" +"Project-Id-Version: TerrariumPI 3.8.0\n" +"POT-Creation-Date: 2018-06-26 22:31+CEST\n" +"PO-Revision-Date: 2018-06-26 22:31+0200\n" "Last-Translator: Joshua (TheYOSH) Rubingh \n" "Language-Team: \n" "Language: en_US\n" @@ -328,99 +328,135 @@ msgstr "Holds the username for authentication with the mailserver if needed." msgid "Holds the password for authentication with the mailserver if needed." msgstr "Holds the password for authentication with the mailserver if needed." -#: terrariumTranslations.py:125 +#: terrariumTranslations.py:112 +msgid "Holds your Twitter consumer key. More information %shere%s" +msgstr "Holds your Twitter consumer key. More information %shere%s" + +#: terrariumTranslations.py:113 +msgid "Holds your Twitter consumer secret. More information %shere%s" +msgstr "Holds your Twitter consumer secret. More information %shere%s" + +#: terrariumTranslations.py:114 +msgid "Holds your Twitter access token. More information %shere%s" +msgstr "Holds your Twitter access token. More information %shere%s" + +#: terrariumTranslations.py:115 +msgid "Holds your Twitter access token secret. More information %shere%s" +msgstr "Holds your Twitter access token secret. More information %shere%s" + +#: terrariumTranslations.py:117 +msgid "Holds the PushOver API token. More information %shere%s" +msgstr "Holds the PushOver API token. More information %shere%s" + +#: terrariumTranslations.py:118 +msgid "Holds the PushOver API user key. More information %shere%s" +msgstr "Holds the PushOver API user key. More information %shere%s" + +#: terrariumTranslations.py:120 +msgid "Holds the Telegram Bot token. More information %shere%s" +msgstr "Holds the Telegram Bot token. More information %shere%s" + +#: terrariumTranslations.py:121 +msgid "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" +msgstr "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" + +#: terrariumTranslations.py:122 +msgid "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." +msgstr "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." + +#: terrariumTranslations.py:126 msgid "Choose your interface language." msgstr "Choose your interface language." -#: terrariumTranslations.py:126 +#: terrariumTranslations.py:127 msgid "Holds the distance type used by distance sensors." msgstr "Holds the distance type used by distance sensors." -#: terrariumTranslations.py:127 terrariumTranslations.py:128 +#: terrariumTranslations.py:128 terrariumTranslations.py:129 msgid "Holds the username which can make changes (Administrator)." msgstr "Holds the username which can make changes (Administrator)." -#: terrariumTranslations.py:129 +#: terrariumTranslations.py:130 msgid "Enter the new password for the administration user. Leaving empty will not change the password!" msgstr "Enter the new password for the administration user. Leaving empty will not change the password!" -#: terrariumTranslations.py:130 +#: terrariumTranslations.py:131 msgid "Enter the current password in order to change the password." msgstr "Enter the current password in order to change the password." -#: terrariumTranslations.py:131 +#: terrariumTranslations.py:132 msgid "Toggle on or off full authentication. When on, you need to authenticate at all times." msgstr "Toggle on or off full authentication. When on, you need to authenticate at all times." -#: terrariumTranslations.py:132 +#: terrariumTranslations.py:133 msgid "Holds the external calendar url." msgstr "Holds the external calendar url." -#: terrariumTranslations.py:133 +#: terrariumTranslations.py:134 msgid "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." msgstr "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." -#: terrariumTranslations.py:134 +#: terrariumTranslations.py:135 msgid "Holds the soundcard that is used for playing audio." msgstr "Holds the soundcard that is used for playing audio." -#: terrariumTranslations.py:135 +#: terrariumTranslations.py:136 msgid "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." msgstr "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." -#: terrariumTranslations.py:136 +#: terrariumTranslations.py:137 msgid "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." msgstr "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." -#: terrariumTranslations.py:137 +#: terrariumTranslations.py:138 msgid "Holds the amount of euro/dollar per 1000 liters water." msgstr "Holds the amount of euro/dollar per 1000 liters water." -#: terrariumTranslations.py:138 +#: terrariumTranslations.py:139 msgid "Choose the temperature indicator. The software will recalculate to the chosen indicator." msgstr "Choose the temperature indicator. The software will recalculate to the chosen indicator." -#: terrariumTranslations.py:139 +#: terrariumTranslations.py:140 msgid "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." msgstr "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." -#: terrariumTranslations.py:140 +#: terrariumTranslations.py:141 msgid "Holds the port number on which the software is listening for connections." msgstr "Holds the port number on which the software is listening for connections." -#: terrariumTranslations.py:141 +#: terrariumTranslations.py:142 msgid "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." msgstr "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." -#: terrariumTranslations.py:145 +#: terrariumTranslations.py:146 msgid "Holds the name of the animal." msgstr "Holds the name of the animal." -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:147 msgid "Holds the type of the animal" msgstr "Holds the type of the animal" -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:148 msgid "Holds the gender of the animal." msgstr "Holds the gender of the animal." -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:149 msgid "Holds the day of birth of the animal." msgstr "Holds the day of birth of the animal." -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:150 msgid "Holds the species name of the animal." msgstr "Holds the species name of the animal." -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:151 msgid "Holds the latin name of the animal." msgstr "Holds the latin name of the animal." -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:152 msgid "Holds a small description about the animal." msgstr "Holds a small description about the animal." -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:153 msgid "Holds a link to more information." msgstr "Holds a link to more information." @@ -462,10 +498,18 @@ msgstr "Player command executed!" msgid "File is not uploaded!" msgstr "File is not uploaded!" +#: terrariumWebserver.py:243 +msgid "File '%s' is uploaded" +msgstr "File '%s' is uploaded" + #: terrariumWebserver.py:243 terrariumWebserver.py:253 msgid "Success!" msgstr "Success!" +#: terrariumWebserver.py:245 +msgid "Duplicate file '%s'" +msgstr "Duplicate file '%s'" + #: terrariumWebserver.py:250 msgid "Action could not be satisfied" msgstr "Action could not be satisfied" @@ -650,7 +694,7 @@ msgid "Files" msgstr "Files" #: views/audio_playlist.tpl:48 views/door_settings.tpl:36 -#: views/notifications.tpl:205 views/profile.tpl:179 +#: views/notifications.tpl:211 views/profile.tpl:179 #: views/sensor_settings.tpl:54 views/switch_settings.tpl:72 #: views/system_environment.tpl:1379 views/system_settings.tpl:184 #: views/webcam_settings.tpl:47 @@ -717,7 +761,7 @@ msgstr "Calendar" #: views/inc/usage_webcams.tpl:88 views/inc/usage_webcams.tpl:163 #: views/notifications.tpl:27 views/notifications.tpl:64 #: views/notifications.tpl:97 views/notifications.tpl:122 -#: views/notifications.tpl:147 views/switch_status.tpl:37 +#: views/notifications.tpl:151 views/switch_status.tpl:37 #: views/system_environment.tpl:27 views/system_environment.tpl:158 #: views/system_environment.tpl:370 views/system_environment.tpl:581 #: views/system_environment.tpl:792 views/system_environment.tpl:1003 @@ -1536,7 +1580,7 @@ msgstr "With the wrench you will get an options menu." #: views/inc/usage_sensors.tpl:17 views/inc/usage_sensors.tpl:203 #: views/inc/usage_switches.tpl:13 views/inc/usage_switches.tpl:160 #: views/inc/usage_webcams.tpl:13 views/inc/usage_webcams.tpl:154 -#: views/notifications.tpl:161 +#: views/notifications.tpl:165 msgid "Title" msgstr "Title" @@ -2185,22 +2229,26 @@ msgid "Bot Token" msgstr "Bot Token" #: views/notifications.tpl:136 -msgid "User id" -msgstr "User id" +msgid "Username" +msgstr "Username" -#: views/notifications.tpl:147 +#: views/notifications.tpl:140 +msgid "Proxy" +msgstr "Proxy" + +#: views/notifications.tpl:151 msgid "Messages" msgstr "Messages" -#: views/notifications.tpl:158 +#: views/notifications.tpl:162 msgid "Trigger" msgstr "Trigger" -#: views/notifications.tpl:164 +#: views/notifications.tpl:168 msgid "Message" msgstr "Message" -#: views/notifications.tpl:167 +#: views/notifications.tpl:171 msgid "Service" msgstr "Service" @@ -2824,38 +2872,6 @@ msgstr "GPIO relay board wiring scheme" msgid "Height in cm" msgstr "Height in cm" -#: Missing text string -msgid "Holds the PushOver API token. More information %shere%s' % ('','" -msgstr "Holds the PushOver API token. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds the PushOver API user key. More information %shere%s' % ('','" -msgstr "Holds the PushOver API user key. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds the Telegram Bot token. More information %shere%s' % ('','" -msgstr "Holds the Telegram Bot token. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds the Telegram userid for receiving messages. More information %shere%s' % ('','" -msgstr "Holds the Telegram userid for receiving messages. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds your Twitter access token. More information %shere%s' % ('','" -msgstr "Holds your Twitter access token. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds your Twitter access token secret. More information %shere%s' % ('','" -msgstr "Holds your Twitter access token secret. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds your Twitter consumer key. More information %shere%s' % ('','" -msgstr "Holds your Twitter consumer key. More information %shere%s' % ('','" - -#: Missing text string -msgid "Holds your Twitter consumer secret. More information %shere%s' % ('','" -msgstr "Holds your Twitter consumer secret. More information %shere%s' % ('','" - #: Missing text string msgid "I2C bus" msgstr "I2C bus" diff --git a/locales/terrariumpi.pot b/locales/terrariumpi.pot index 4ae08c872..9c0231e70 100644 --- a/locales/terrariumpi.pot +++ b/locales/terrariumpi.pot @@ -4,8 +4,8 @@ # msgid "" msgstr "" -"Project-Id-Version: TerrariumPI 3.6.0\n" -"POT-Creation-Date: 2018-06-15 21:59+CEST\n" +"Project-Id-Version: TerrariumPI 3.8.0\n" +"POT-Creation-Date: 2018-06-26 22:31+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -327,99 +327,135 @@ msgstr "" msgid "Holds the password for authentication with the mailserver if needed." msgstr "" -#: terrariumTranslations.py:125 -msgid "Choose your interface language." +#: terrariumTranslations.py:112 +msgid "Holds your Twitter consumer key. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:113 +msgid "Holds your Twitter consumer secret. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:114 +msgid "Holds your Twitter access token. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:115 +msgid "Holds your Twitter access token secret. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:117 +msgid "Holds the PushOver API token. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:118 +msgid "Holds the PushOver API user key. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:120 +msgid "Holds the Telegram Bot token. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:121 +msgid "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:122 +msgid "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." msgstr "" #: terrariumTranslations.py:126 +msgid "Choose your interface language." +msgstr "" + +#: terrariumTranslations.py:127 msgid "Holds the distance type used by distance sensors." msgstr "" -#: terrariumTranslations.py:127 terrariumTranslations.py:128 +#: terrariumTranslations.py:128 terrariumTranslations.py:129 msgid "Holds the username which can make changes (Administrator)." msgstr "" -#: terrariumTranslations.py:129 +#: terrariumTranslations.py:130 msgid "Enter the new password for the administration user. Leaving empty will not change the password!" msgstr "" -#: terrariumTranslations.py:130 +#: terrariumTranslations.py:131 msgid "Enter the current password in order to change the password." msgstr "" -#: terrariumTranslations.py:131 +#: terrariumTranslations.py:132 msgid "Toggle on or off full authentication. When on, you need to authenticate at all times." msgstr "" -#: terrariumTranslations.py:132 +#: terrariumTranslations.py:133 msgid "Holds the external calendar url." msgstr "" -#: terrariumTranslations.py:133 +#: terrariumTranslations.py:134 msgid "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." msgstr "" -#: terrariumTranslations.py:134 +#: terrariumTranslations.py:135 msgid "Holds the soundcard that is used for playing audio." msgstr "" -#: terrariumTranslations.py:135 +#: terrariumTranslations.py:136 msgid "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." msgstr "" -#: terrariumTranslations.py:136 +#: terrariumTranslations.py:137 msgid "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." msgstr "" -#: terrariumTranslations.py:137 +#: terrariumTranslations.py:138 msgid "Holds the amount of euro/dollar per 1000 liters water." msgstr "" -#: terrariumTranslations.py:138 +#: terrariumTranslations.py:139 msgid "Choose the temperature indicator. The software will recalculate to the chosen indicator." msgstr "" -#: terrariumTranslations.py:139 +#: terrariumTranslations.py:140 msgid "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." msgstr "" -#: terrariumTranslations.py:140 +#: terrariumTranslations.py:141 msgid "Holds the port number on which the software is listening for connections." msgstr "" -#: terrariumTranslations.py:141 +#: terrariumTranslations.py:142 msgid "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." msgstr "" -#: terrariumTranslations.py:145 +#: terrariumTranslations.py:146 msgid "Holds the name of the animal." msgstr "" -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:147 msgid "Holds the type of the animal" msgstr "" -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:148 msgid "Holds the gender of the animal." msgstr "" -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:149 msgid "Holds the day of birth of the animal." msgstr "" -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:150 msgid "Holds the species name of the animal." msgstr "" -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:151 msgid "Holds the latin name of the animal." msgstr "" -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:152 msgid "Holds a small description about the animal." msgstr "" -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:153 msgid "Holds a link to more information." msgstr "" @@ -461,10 +497,18 @@ msgstr "" msgid "File is not uploaded!" msgstr "" +#: terrariumWebserver.py:243 +msgid "File '%s' is uploaded" +msgstr "" + #: terrariumWebserver.py:243 terrariumWebserver.py:253 msgid "Success!" msgstr "" +#: terrariumWebserver.py:245 +msgid "Duplicate file '%s'" +msgstr "" + #: terrariumWebserver.py:250 msgid "Action could not be satisfied" msgstr "" @@ -649,7 +693,7 @@ msgid "Files" msgstr "" #: views/audio_playlist.tpl:48 views/door_settings.tpl:36 -#: views/notifications.tpl:205 views/profile.tpl:179 +#: views/notifications.tpl:211 views/profile.tpl:179 #: views/sensor_settings.tpl:54 views/switch_settings.tpl:72 #: views/system_environment.tpl:1379 views/system_settings.tpl:184 #: views/webcam_settings.tpl:47 @@ -716,7 +760,7 @@ msgstr "" #: views/inc/usage_webcams.tpl:88 views/inc/usage_webcams.tpl:163 #: views/notifications.tpl:27 views/notifications.tpl:64 #: views/notifications.tpl:97 views/notifications.tpl:122 -#: views/notifications.tpl:147 views/switch_status.tpl:37 +#: views/notifications.tpl:151 views/switch_status.tpl:37 #: views/system_environment.tpl:27 views/system_environment.tpl:158 #: views/system_environment.tpl:370 views/system_environment.tpl:581 #: views/system_environment.tpl:792 views/system_environment.tpl:1003 @@ -1535,7 +1579,7 @@ msgstr "" #: views/inc/usage_sensors.tpl:17 views/inc/usage_sensors.tpl:203 #: views/inc/usage_switches.tpl:13 views/inc/usage_switches.tpl:160 #: views/inc/usage_webcams.tpl:13 views/inc/usage_webcams.tpl:154 -#: views/notifications.tpl:161 +#: views/notifications.tpl:165 msgid "Title" msgstr "" @@ -2184,22 +2228,26 @@ msgid "Bot Token" msgstr "" #: views/notifications.tpl:136 -msgid "User id" +msgid "Username" +msgstr "" + +#: views/notifications.tpl:140 +msgid "Proxy" msgstr "" -#: views/notifications.tpl:147 +#: views/notifications.tpl:151 msgid "Messages" msgstr "" -#: views/notifications.tpl:158 +#: views/notifications.tpl:162 msgid "Trigger" msgstr "" -#: views/notifications.tpl:164 +#: views/notifications.tpl:168 msgid "Message" msgstr "" -#: views/notifications.tpl:167 +#: views/notifications.tpl:171 msgid "Service" msgstr "" @@ -2823,38 +2871,6 @@ msgstr "" msgid "Height in cm" msgstr "" -#: Missing text string -msgid "Holds the PushOver API token. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds the PushOver API user key. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds the Telegram Bot token. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds the Telegram userid for receiving messages. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds your Twitter access token. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds your Twitter access token secret. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds your Twitter consumer key. More information %shere%s' % ('','" -msgstr "" - -#: Missing text string -msgid "Holds your Twitter consumer secret. More information %shere%s' % ('','" -msgstr "" - #: Missing text string msgid "I2C bus" msgstr "" diff --git a/terrariumTranslations.py b/terrariumTranslations.py index 2768a1f06..6b29f5e9a 100644 --- a/terrariumTranslations.py +++ b/terrariumTranslations.py @@ -109,16 +109,17 @@ def __load(self): self.translations['notification_email_email_username'] = _('Holds the username for authentication with the mailserver if needed.') self.translations['notification_email_email_password'] = _('Holds the password for authentication with the mailserver if needed.') - self.translations['notification_twitter_consumer_key'] = _('Holds your Twitter consumer key. More information %shere%s' % ('','')) - self.translations['notification_twitter_consumer_secret'] = _('Holds your Twitter consumer secret. More information %shere%s' % ('','')) - self.translations['notification_twitter_access_token'] = _('Holds your Twitter access token. More information %shere%s' % ('','')) - self.translations['notification_twitter_access_token_secret'] = _('Holds your Twitter access token secret. More information %shere%s' % ('','')) + self.translations['notification_twitter_consumer_key'] = _('Holds your Twitter consumer key. More information %shere%s') % ('','') + self.translations['notification_twitter_consumer_secret'] = _('Holds your Twitter consumer secret. More information %shere%s') % ('','') + self.translations['notification_twitter_access_token'] = _('Holds your Twitter access token. More information %shere%s') % ('','') + self.translations['notification_twitter_access_token_secret'] = _('Holds your Twitter access token secret. More information %shere%s') % ('','') - self.translations['notification_pushover_api_token'] = _('Holds the PushOver API token. More information %shere%s' % ('','')) - self.translations['notification_pushover_user_key'] = _('Holds the PushOver API user key. More information %shere%s' % ('','')) + self.translations['notification_pushover_api_token'] = _('Holds the PushOver API token. More information %shere%s') % ('','') + self.translations['notification_pushover_user_key'] = _('Holds the PushOver API user key. More information %shere%s') % ('','') - self.translations['notification_telegram_bot_token'] = _('Holds the Telegram Bot token. More information %shere%s' % ('','')) - self.translations['notification_telegram_username'] = _('Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s' % ('','')) + self.translations['notification_telegram_bot_token'] = _('Holds the Telegram Bot token. More information %shere%s') % ('','') + self.translations['notification_telegram_username'] = _('Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s') % ('','') + self.translations['notification_telegram_proxy'] = _('Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema.') # End notifications # System @@ -151,7 +152,7 @@ def __load(self): self.translations['profile_description'] = _('Holds a small description about the animal.') self.translations['profile_more_information'] = _('Holds a link to more information.') - logger.info('Loaded TerrariumPI %s translations' % (len(self.translations),)) + logger.info('Loaded TerrariumPI with %s translations' % (len(self.translations),)) def get_translation(self,translation): if translation in self.translations: diff --git a/terrariumWebserver.py b/terrariumWebserver.py index ae8dce3cd..055773fd3 100644 --- a/terrariumWebserver.py +++ b/terrariumWebserver.py @@ -240,9 +240,9 @@ def __upload_audio_file(self): try: upload.save(terrariumAudioPlayer.AUDIO_FOLDER) self.__terrariumEngine.reload_audio_files() - result = {'ok' : True, 'title' : _('Success!'), 'message' : _('File \'%s\' is uploaded' % (upload.filename,))} + result = {'ok' : True, 'title' : _('Success!'), 'message' : _('File \'%s\' is uploaded') % (upload.filename,)} except IOError, message: - result['message'] = _('Duplicate file \'%s\'' % (upload.filename,)) + result['message'] = _('Duplicate file \'%s\'') % (upload.filename,) return result diff --git a/views/notifications.tpl b/views/notifications.tpl index dc82c466e..cc9a97362 100644 --- a/views/notifications.tpl +++ b/views/notifications.tpl @@ -171,7 +171,7 @@ -% for message in notifications.get_messages(): + % for message in notifications.get_messages():
{{message['id'].replace('_',' ').capitalize()}} @@ -198,7 +198,7 @@
-% end + % end From a0c8f3c304b1d046339e5f5718d105becdbbd681 Mon Sep 17 00:00:00 2001 From: TheYOSH Date: Wed, 27 Jun 2018 19:48:09 +0000 Subject: [PATCH 19/33] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 432653935..e3c770bb6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TerrariumPI 3.7.0 +# TerrariumPI 3.8.0 Software for cheap home automation of your reptile terrarium or any other enclosed environment. With this software you are able to control for example a terrarium so that the temperature and humidity is of a constant value. Controlling the temperature can be done with heat lights, external heating or cooling system. As long as there is one temperature sensor available the software is able to keep a constant temperature. For humidity control there is support for a spraying system. The sprayer can be configured to spray for an X amount of seconds and there is a minumal period between two spray actions. Use at least one humitidy sensors to get a constant humidity value. In order to lower the humidity you can add a dehumidifier. From 927ffdf49956e64deea4b1b827a0697ab6cef14d Mon Sep 17 00:00:00 2001 From: theyosh Date: Wed, 27 Jun 2018 22:45:11 +0200 Subject: [PATCH 20/33] Fix database recovery --- terrariumCollector.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/terrariumCollector.py b/terrariumCollector.py index cbd284ce6..174378342 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -4,7 +4,6 @@ import sqlite3 import time -import json import copy import os @@ -208,6 +207,7 @@ def __recover(self): # Reconnect will recreate the db logger.warn('TerrariumPI Collecter recovery mode starts reconnecting database to create a new clean database at %s', (terrariumCollector.DATABASE,)) self.__connect() + self.__create_database_structure() cur = self.db.cursor() # Load the SQL data back to db cur.executescript(sqldump) @@ -439,18 +439,9 @@ def get_history(self, parameters = [], starttime = None, stoptime = None): with self.db as db: cur = db.cursor() for row in cur.execute(sql, filters): - if row['type'] in ['switches','doors'] and row['timestamp2'] is not None and '' != row['timestamp2'] and row['timestamp2'] < stoptime: continue - - - #if logtype == 'switches' and len(row) == len(fields)+1: - # for field in fields: - # history[field] = row[field] - - # return history - if row['type'] not in history: history[row['type']] = {} From db9152621a6fa50ecdf06a8fb4f8e657dad9137b Mon Sep 17 00:00:00 2001 From: theyosh Date: Wed, 27 Jun 2018 22:45:29 +0200 Subject: [PATCH 21/33] Fix missing dimmer step setting --- terrariumEngine.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terrariumEngine.py b/terrariumEngine.py index cbef54e02..7929f0da3 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -224,6 +224,8 @@ def __load_power_switches(self,data = None): if 'dimmer_duration' in switchdata and switchdata['dimmer_duration'] is not None: power_switch.set_dimmer_duration(switchdata['dimmer_duration']) + if 'dimmer_step' in switchdata and switchdata['dimmer_step'] is not None: + power_switch.set_dimmer_step(switchdata['dimmer_step']) if 'dimmer_on_duration' in switchdata and switchdata['dimmer_on_duration'] is not None: power_switch.set_dimmer_on_duration(switchdata['dimmer_on_duration']) if 'dimmer_on_percentage' in switchdata and switchdata['dimmer_on_percentage'] is not None: From 34fd974a26c89a3ae8b60a10170df8cff0f8f618 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 19:06:21 +0200 Subject: [PATCH 22/33] Added support for LCD screens through notification system. #164 #101 --- locales/en_US/LC_MESSAGES/terrariumpi.mo | Bin 74041 -> 74769 bytes locales/en_US/LC_MESSAGES/terrariumpi.po | 167 +++++++------ locales/terrariumpi.pot | 165 ++++++++----- locales/update_translations.sh | 2 +- terrariumEngine.py | 8 + terrariumLCD.py | 288 +++++++++++++++++++++++ terrariumNotification.py | 51 ++-- terrariumTranslations.py | 4 + terrariumUtils.py | 7 + views/notifications.tpl | 50 +++- 10 files changed, 595 insertions(+), 147 deletions(-) create mode 100644 terrariumLCD.py diff --git a/locales/en_US/LC_MESSAGES/terrariumpi.mo b/locales/en_US/LC_MESSAGES/terrariumpi.mo index 6bf93e102dc60b6fe2e27f4024d238a75c688e27..23e99cdf0fa15cd06e6e3faa23e6a5f2cc02dcb4 100644 GIT binary patch delta 15460 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 B#s&ZY delta 14786 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\n" "Language-Team: \n" "Language: en_US\n" @@ -329,134 +329,146 @@ msgid "Holds the password for authentication with the mailserver if needed." msgstr "Holds the password for authentication with the mailserver if needed." #: terrariumTranslations.py:112 +msgid "Holds the I2C address of the LCD screen. Use the value found with i2cdetect. Add ,[NR] to change the I2C bus." +msgstr "Holds the I2C address of the LCD screen. Use the value found with i2cdetect. Add ,[NR] to change the I2C bus." + +#: terrariumTranslations.py:113 +msgid "Holds the LCD screen resolution." +msgstr "Holds the LCD screen resolution." + +#: terrariumTranslations.py:114 +msgid "Reserve first LCD line for static title." +msgstr "Reserve first LCD line for static title." + +#: terrariumTranslations.py:116 msgid "Holds your Twitter consumer key. More information %shere%s" msgstr "Holds your Twitter consumer key. More information %shere%s" -#: terrariumTranslations.py:113 +#: terrariumTranslations.py:117 msgid "Holds your Twitter consumer secret. More information %shere%s" msgstr "Holds your Twitter consumer secret. More information %shere%s" -#: terrariumTranslations.py:114 +#: terrariumTranslations.py:118 msgid "Holds your Twitter access token. More information %shere%s" msgstr "Holds your Twitter access token. More information %shere%s" -#: terrariumTranslations.py:115 +#: terrariumTranslations.py:119 msgid "Holds your Twitter access token secret. More information %shere%s" msgstr "Holds your Twitter access token secret. More information %shere%s" -#: terrariumTranslations.py:117 +#: terrariumTranslations.py:121 msgid "Holds the PushOver API token. More information %shere%s" msgstr "Holds the PushOver API token. More information %shere%s" -#: terrariumTranslations.py:118 +#: terrariumTranslations.py:122 msgid "Holds the PushOver API user key. More information %shere%s" msgstr "Holds the PushOver API user key. More information %shere%s" -#: terrariumTranslations.py:120 +#: terrariumTranslations.py:124 msgid "Holds the Telegram Bot token. More information %shere%s" msgstr "Holds the Telegram Bot token. More information %shere%s" -#: terrariumTranslations.py:121 +#: terrariumTranslations.py:125 msgid "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" msgstr "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" -#: terrariumTranslations.py:122 +#: terrariumTranslations.py:126 msgid "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." msgstr "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." -#: terrariumTranslations.py:126 +#: terrariumTranslations.py:130 msgid "Choose your interface language." msgstr "Choose your interface language." -#: terrariumTranslations.py:127 +#: terrariumTranslations.py:131 msgid "Holds the distance type used by distance sensors." msgstr "Holds the distance type used by distance sensors." -#: terrariumTranslations.py:128 terrariumTranslations.py:129 +#: terrariumTranslations.py:132 terrariumTranslations.py:133 msgid "Holds the username which can make changes (Administrator)." msgstr "Holds the username which can make changes (Administrator)." -#: terrariumTranslations.py:130 +#: terrariumTranslations.py:134 msgid "Enter the new password for the administration user. Leaving empty will not change the password!" msgstr "Enter the new password for the administration user. Leaving empty will not change the password!" -#: terrariumTranslations.py:131 +#: terrariumTranslations.py:135 msgid "Enter the current password in order to change the password." msgstr "Enter the current password in order to change the password." -#: terrariumTranslations.py:132 +#: terrariumTranslations.py:136 msgid "Toggle on or off full authentication. When on, you need to authenticate at all times." msgstr "Toggle on or off full authentication. When on, you need to authenticate at all times." -#: terrariumTranslations.py:133 +#: terrariumTranslations.py:137 msgid "Holds the external calendar url." msgstr "Holds the external calendar url." -#: terrariumTranslations.py:134 +#: terrariumTranslations.py:138 msgid "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." msgstr "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." -#: terrariumTranslations.py:135 +#: terrariumTranslations.py:139 msgid "Holds the soundcard that is used for playing audio." msgstr "Holds the soundcard that is used for playing audio." -#: terrariumTranslations.py:136 +#: terrariumTranslations.py:140 msgid "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." msgstr "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." -#: terrariumTranslations.py:137 +#: terrariumTranslations.py:141 msgid "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." msgstr "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." -#: terrariumTranslations.py:138 +#: terrariumTranslations.py:142 msgid "Holds the amount of euro/dollar per 1000 liters water." msgstr "Holds the amount of euro/dollar per 1000 liters water." -#: terrariumTranslations.py:139 +#: terrariumTranslations.py:143 msgid "Choose the temperature indicator. The software will recalculate to the chosen indicator." msgstr "Choose the temperature indicator. The software will recalculate to the chosen indicator." -#: terrariumTranslations.py:140 +#: terrariumTranslations.py:144 msgid "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." msgstr "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." -#: terrariumTranslations.py:141 +#: terrariumTranslations.py:145 msgid "Holds the port number on which the software is listening for connections." msgstr "Holds the port number on which the software is listening for connections." -#: terrariumTranslations.py:142 +#: terrariumTranslations.py:146 msgid "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." msgstr "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:150 msgid "Holds the name of the animal." msgstr "Holds the name of the animal." -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:151 msgid "Holds the type of the animal" msgstr "Holds the type of the animal" -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:152 msgid "Holds the gender of the animal." msgstr "Holds the gender of the animal." -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:153 msgid "Holds the day of birth of the animal." msgstr "Holds the day of birth of the animal." -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:154 msgid "Holds the species name of the animal." msgstr "Holds the species name of the animal." -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:155 msgid "Holds the latin name of the animal." msgstr "Holds the latin name of the animal." -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:156 msgid "Holds a small description about the animal." msgstr "Holds a small description about the animal." -#: terrariumTranslations.py:153 +#: terrariumTranslations.py:157 msgid "Holds a link to more information." msgstr "Holds a link to more information." @@ -694,7 +706,7 @@ msgid "Files" msgstr "Files" #: views/audio_playlist.tpl:48 views/door_settings.tpl:36 -#: views/notifications.tpl:211 views/profile.tpl:179 +#: views/notifications.tpl:250 views/profile.tpl:179 #: views/sensor_settings.tpl:54 views/switch_settings.tpl:72 #: views/system_environment.tpl:1379 views/system_settings.tpl:184 #: views/webcam_settings.tpl:47 @@ -760,15 +772,16 @@ msgstr "Calendar" #: views/inc/usage_weather.tpl:94 views/inc/usage_webcams.tpl:46 #: views/inc/usage_webcams.tpl:88 views/inc/usage_webcams.tpl:163 #: views/notifications.tpl:27 views/notifications.tpl:64 -#: views/notifications.tpl:97 views/notifications.tpl:122 -#: views/notifications.tpl:151 views/switch_status.tpl:37 -#: views/system_environment.tpl:27 views/system_environment.tpl:158 -#: views/system_environment.tpl:370 views/system_environment.tpl:581 -#: views/system_environment.tpl:792 views/system_environment.tpl:1003 -#: views/system_environment.tpl:1214 views/system_log.tpl:15 -#: views/system_status.tpl:32 views/system_status.tpl:88 -#: views/system_status.tpl:144 views/system_status.tpl:200 -#: views/system_status.tpl:256 views/weather.tpl:16 views/webcam.tpl:20 +#: views/notifications.tpl:103 views/notifications.tpl:136 +#: views/notifications.tpl:161 views/notifications.tpl:190 +#: views/switch_status.tpl:37 views/system_environment.tpl:27 +#: views/system_environment.tpl:158 views/system_environment.tpl:370 +#: views/system_environment.tpl:581 views/system_environment.tpl:792 +#: views/system_environment.tpl:1003 views/system_environment.tpl:1214 +#: views/system_log.tpl:15 views/system_status.tpl:32 +#: views/system_status.tpl:88 views/system_status.tpl:144 +#: views/system_status.tpl:200 views/system_status.tpl:256 +#: views/weather.tpl:16 views/webcam.tpl:20 msgid "Settings" msgstr "Settings" @@ -835,11 +848,12 @@ msgstr "mode" #: views/inc/usage_environment.tpl:25 views/inc/usage_environment.tpl:121 #: views/inc/usage_environment.tpl:129 views/inc/usage_environment.tpl:213 #: views/inc/usage_environment.tpl:221 views/inc/usage_environment.tpl:307 -#: views/inc/usage_environment.tpl:315 views/switch_settings.tpl:144 -#: views/system_environment.tpl:41 views/system_environment.tpl:172 -#: views/system_environment.tpl:384 views/system_environment.tpl:595 -#: views/system_environment.tpl:806 views/system_environment.tpl:1017 -#: views/system_environment.tpl:1228 views/webcam_settings.tpl:120 +#: views/inc/usage_environment.tpl:315 views/notifications.tpl:91 +#: views/switch_settings.tpl:144 views/system_environment.tpl:41 +#: views/system_environment.tpl:172 views/system_environment.tpl:384 +#: views/system_environment.tpl:595 views/system_environment.tpl:806 +#: views/system_environment.tpl:1017 views/system_environment.tpl:1228 +#: views/webcam_settings.tpl:120 msgid "Disabled" msgstr "Disabled" @@ -1580,7 +1594,7 @@ msgstr "With the wrench you will get an options menu." #: views/inc/usage_sensors.tpl:17 views/inc/usage_sensors.tpl:203 #: views/inc/usage_switches.tpl:13 views/inc/usage_switches.tpl:160 #: views/inc/usage_webcams.tpl:13 views/inc/usage_webcams.tpl:154 -#: views/notifications.tpl:165 +#: views/notifications.tpl:87 views/notifications.tpl:204 msgid "Title" msgstr "Title" @@ -1681,7 +1695,8 @@ msgstr "All fields with a %s are required." #: views/inc/usage_environment.tpl:25 views/inc/usage_environment.tpl:121 #: views/inc/usage_environment.tpl:129 views/inc/usage_environment.tpl:213 #: views/inc/usage_environment.tpl:221 views/inc/usage_environment.tpl:307 -#: views/inc/usage_environment.tpl:315 views/switch_settings.tpl:143 +#: views/inc/usage_environment.tpl:315 views/notifications.tpl:90 +#: views/switch_settings.tpl:143 msgid "Enabled" msgstr "Enabled" @@ -2189,66 +2204,86 @@ msgid "SMTP password" msgstr "SMTP password" #: views/notifications.tpl:64 +msgid "LCD" +msgstr "LCD" + +#: views/notifications.tpl:74 +msgid "I2C address" +msgstr "I2C address" + +#: views/notifications.tpl:78 +msgid "Screen resolution" +msgstr "Screen resolution" + +#: views/notifications.tpl:81 +msgid "16 Characters, 2 Lines" +msgstr "16 Characters, 2 Lines" + +#: views/notifications.tpl:82 +msgid "20 Characters, 4 Lines" +msgstr "20 Characters, 4 Lines" + +#: views/notifications.tpl:103 msgid "Twitter" msgstr "Twitter" -#: views/notifications.tpl:74 +#: views/notifications.tpl:113 msgid "Consumer key" msgstr "Consumer key" -#: views/notifications.tpl:78 +#: views/notifications.tpl:117 msgid "Consumer secret" msgstr "Consumer secret" -#: views/notifications.tpl:82 +#: views/notifications.tpl:121 msgid "Access token" msgstr "Access token" -#: views/notifications.tpl:86 +#: views/notifications.tpl:125 msgid "Access token secret" msgstr "Access token secret" -#: views/notifications.tpl:97 +#: views/notifications.tpl:136 msgid "Pushover" msgstr "Pushover" -#: views/notifications.tpl:107 +#: views/notifications.tpl:146 msgid "API Token" msgstr "API Token" -#: views/notifications.tpl:111 +#: views/notifications.tpl:150 msgid "User key" msgstr "User key" -#: views/notifications.tpl:122 +#: views/notifications.tpl:161 msgid "Telegram" msgstr "Telegram" -#: views/notifications.tpl:132 +#: views/notifications.tpl:171 msgid "Bot Token" msgstr "Bot Token" -#: views/notifications.tpl:136 +#: views/notifications.tpl:175 msgid "Username" msgstr "Username" -#: views/notifications.tpl:140 +#: views/notifications.tpl:179 msgid "Proxy" msgstr "Proxy" -#: views/notifications.tpl:151 +#: views/notifications.tpl:190 msgid "Messages" msgstr "Messages" -#: views/notifications.tpl:162 +#: views/notifications.tpl:201 msgid "Trigger" msgstr "Trigger" -#: views/notifications.tpl:168 +#: views/notifications.tpl:207 msgid "Message" msgstr "Message" -#: views/notifications.tpl:171 +#: views/notifications.tpl:210 msgid "Service" msgstr "Service" diff --git a/locales/terrariumpi.pot b/locales/terrariumpi.pot index 9c0231e70..53cc2e3f9 100644 --- a/locales/terrariumpi.pot +++ b/locales/terrariumpi.pot @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: TerrariumPI 3.8.0\n" -"POT-Creation-Date: 2018-06-26 22:31+CEST\n" +"POT-Creation-Date: 2018-06-30 18:33+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -328,134 +328,146 @@ msgid "Holds the password for authentication with the mailserver if needed." msgstr "" #: terrariumTranslations.py:112 -msgid "Holds your Twitter consumer key. More information %shere%s" +msgid "Holds the I2C address of the LCD screen. Use the value found with i2cdetect. Add ,[NR] to change the I2C bus." msgstr "" #: terrariumTranslations.py:113 -msgid "Holds your Twitter consumer secret. More information %shere%s" +msgid "Holds the LCD screen resolution." msgstr "" #: terrariumTranslations.py:114 +msgid "Reserve first LCD line for static title." +msgstr "" + +#: terrariumTranslations.py:116 +msgid "Holds your Twitter consumer key. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:117 +msgid "Holds your Twitter consumer secret. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:118 msgid "Holds your Twitter access token. More information %shere%s" msgstr "" -#: terrariumTranslations.py:115 +#: terrariumTranslations.py:119 msgid "Holds your Twitter access token secret. More information %shere%s" msgstr "" -#: terrariumTranslations.py:117 +#: terrariumTranslations.py:121 msgid "Holds the PushOver API token. More information %shere%s" msgstr "" -#: terrariumTranslations.py:118 +#: terrariumTranslations.py:122 msgid "Holds the PushOver API user key. More information %shere%s" msgstr "" -#: terrariumTranslations.py:120 +#: terrariumTranslations.py:124 msgid "Holds the Telegram Bot token. More information %shere%s" msgstr "" -#: terrariumTranslations.py:121 +#: terrariumTranslations.py:125 msgid "Holds the Telegram username that is allowed for receiving messages. Can be multiple usernames seperated by a comma. More information %shere%s" msgstr "" -#: terrariumTranslations.py:122 +#: terrariumTranslations.py:126 msgid "Holds the proxy address in form of [schema]://[user]:[password]@[server.com]:[port]. Can either be socks5 or http(s) for schema." msgstr "" -#: terrariumTranslations.py:126 +#: terrariumTranslations.py:130 msgid "Choose your interface language." msgstr "" -#: terrariumTranslations.py:127 +#: terrariumTranslations.py:131 msgid "Holds the distance type used by distance sensors." msgstr "" -#: terrariumTranslations.py:128 terrariumTranslations.py:129 +#: terrariumTranslations.py:132 terrariumTranslations.py:133 msgid "Holds the username which can make changes (Administrator)." msgstr "" -#: terrariumTranslations.py:130 +#: terrariumTranslations.py:134 msgid "Enter the new password for the administration user. Leaving empty will not change the password!" msgstr "" -#: terrariumTranslations.py:131 +#: terrariumTranslations.py:135 msgid "Enter the current password in order to change the password." msgstr "" -#: terrariumTranslations.py:132 +#: terrariumTranslations.py:136 msgid "Toggle on or off full authentication. When on, you need to authenticate at all times." msgstr "" -#: terrariumTranslations.py:133 +#: terrariumTranslations.py:137 msgid "Holds the external calendar url." msgstr "" -#: terrariumTranslations.py:134 +#: terrariumTranslations.py:138 msgid "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." msgstr "" -#: terrariumTranslations.py:135 +#: terrariumTranslations.py:139 msgid "Holds the soundcard that is used for playing audio." msgstr "" -#: terrariumTranslations.py:136 +#: terrariumTranslations.py:140 msgid "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." msgstr "" -#: terrariumTranslations.py:137 +#: terrariumTranslations.py:141 msgid "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." msgstr "" -#: terrariumTranslations.py:138 +#: terrariumTranslations.py:142 msgid "Holds the amount of euro/dollar per 1000 liters water." msgstr "" -#: terrariumTranslations.py:139 +#: terrariumTranslations.py:143 msgid "Choose the temperature indicator. The software will recalculate to the chosen indicator." msgstr "" -#: terrariumTranslations.py:140 +#: terrariumTranslations.py:144 msgid "Holds the host name or IP address on which the software will listen for connections. Enter :: for all addresses to bind." msgstr "" -#: terrariumTranslations.py:141 +#: terrariumTranslations.py:145 msgid "Holds the port number on which the software is listening for connections." msgstr "" -#: terrariumTranslations.py:142 +#: terrariumTranslations.py:146 msgid "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." msgstr "" -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:150 msgid "Holds the name of the animal." msgstr "" -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:151 msgid "Holds the type of the animal" msgstr "" -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:152 msgid "Holds the gender of the animal." msgstr "" -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:153 msgid "Holds the day of birth of the animal." msgstr "" -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:154 msgid "Holds the species name of the animal." msgstr "" -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:155 msgid "Holds the latin name of the animal." msgstr "" -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:156 msgid "Holds a small description about the animal." msgstr "" -#: terrariumTranslations.py:153 +#: terrariumTranslations.py:157 msgid "Holds a link to more information." msgstr "" @@ -693,7 +705,7 @@ msgid "Files" msgstr "" #: views/audio_playlist.tpl:48 views/door_settings.tpl:36 -#: views/notifications.tpl:211 views/profile.tpl:179 +#: views/notifications.tpl:250 views/profile.tpl:179 #: views/sensor_settings.tpl:54 views/switch_settings.tpl:72 #: views/system_environment.tpl:1379 views/system_settings.tpl:184 #: views/webcam_settings.tpl:47 @@ -759,15 +771,16 @@ msgstr "" #: views/inc/usage_weather.tpl:94 views/inc/usage_webcams.tpl:46 #: views/inc/usage_webcams.tpl:88 views/inc/usage_webcams.tpl:163 #: views/notifications.tpl:27 views/notifications.tpl:64 -#: views/notifications.tpl:97 views/notifications.tpl:122 -#: views/notifications.tpl:151 views/switch_status.tpl:37 -#: views/system_environment.tpl:27 views/system_environment.tpl:158 -#: views/system_environment.tpl:370 views/system_environment.tpl:581 -#: views/system_environment.tpl:792 views/system_environment.tpl:1003 -#: views/system_environment.tpl:1214 views/system_log.tpl:15 -#: views/system_status.tpl:32 views/system_status.tpl:88 -#: views/system_status.tpl:144 views/system_status.tpl:200 -#: views/system_status.tpl:256 views/weather.tpl:16 views/webcam.tpl:20 +#: views/notifications.tpl:103 views/notifications.tpl:136 +#: views/notifications.tpl:161 views/notifications.tpl:190 +#: views/switch_status.tpl:37 views/system_environment.tpl:27 +#: views/system_environment.tpl:158 views/system_environment.tpl:370 +#: views/system_environment.tpl:581 views/system_environment.tpl:792 +#: views/system_environment.tpl:1003 views/system_environment.tpl:1214 +#: views/system_log.tpl:15 views/system_status.tpl:32 +#: views/system_status.tpl:88 views/system_status.tpl:144 +#: views/system_status.tpl:200 views/system_status.tpl:256 +#: views/weather.tpl:16 views/webcam.tpl:20 msgid "Settings" msgstr "" @@ -834,11 +847,12 @@ msgstr "" #: views/inc/usage_environment.tpl:25 views/inc/usage_environment.tpl:121 #: views/inc/usage_environment.tpl:129 views/inc/usage_environment.tpl:213 #: views/inc/usage_environment.tpl:221 views/inc/usage_environment.tpl:307 -#: views/inc/usage_environment.tpl:315 views/switch_settings.tpl:144 -#: views/system_environment.tpl:41 views/system_environment.tpl:172 -#: views/system_environment.tpl:384 views/system_environment.tpl:595 -#: views/system_environment.tpl:806 views/system_environment.tpl:1017 -#: views/system_environment.tpl:1228 views/webcam_settings.tpl:120 +#: views/inc/usage_environment.tpl:315 views/notifications.tpl:91 +#: views/switch_settings.tpl:144 views/system_environment.tpl:41 +#: views/system_environment.tpl:172 views/system_environment.tpl:384 +#: views/system_environment.tpl:595 views/system_environment.tpl:806 +#: views/system_environment.tpl:1017 views/system_environment.tpl:1228 +#: views/webcam_settings.tpl:120 msgid "Disabled" msgstr "" @@ -1579,7 +1593,7 @@ msgstr "" #: views/inc/usage_sensors.tpl:17 views/inc/usage_sensors.tpl:203 #: views/inc/usage_switches.tpl:13 views/inc/usage_switches.tpl:160 #: views/inc/usage_webcams.tpl:13 views/inc/usage_webcams.tpl:154 -#: views/notifications.tpl:165 +#: views/notifications.tpl:87 views/notifications.tpl:204 msgid "Title" msgstr "" @@ -1680,7 +1694,8 @@ msgstr "" #: views/inc/usage_environment.tpl:25 views/inc/usage_environment.tpl:121 #: views/inc/usage_environment.tpl:129 views/inc/usage_environment.tpl:213 #: views/inc/usage_environment.tpl:221 views/inc/usage_environment.tpl:307 -#: views/inc/usage_environment.tpl:315 views/switch_settings.tpl:143 +#: views/inc/usage_environment.tpl:315 views/notifications.tpl:90 +#: views/switch_settings.tpl:143 msgid "Enabled" msgstr "" @@ -2188,66 +2203,86 @@ msgid "SMTP password" msgstr "" #: views/notifications.tpl:64 -msgid "Twitter" +msgid "LCD" msgstr "" #: views/notifications.tpl:74 -msgid "Consumer key" +msgid "I2C address" msgstr "" #: views/notifications.tpl:78 -msgid "Consumer secret" +msgid "Screen resolution" +msgstr "" + +#: views/notifications.tpl:81 +msgid "16 Characters, 2 Lines" msgstr "" #: views/notifications.tpl:82 +msgid "20 Characters, 4 Lines" +msgstr "" + +#: views/notifications.tpl:103 +msgid "Twitter" +msgstr "" + +#: views/notifications.tpl:113 +msgid "Consumer key" +msgstr "" + +#: views/notifications.tpl:117 +msgid "Consumer secret" +msgstr "" + +#: views/notifications.tpl:121 msgid "Access token" msgstr "" -#: views/notifications.tpl:86 +#: views/notifications.tpl:125 msgid "Access token secret" msgstr "" -#: views/notifications.tpl:97 +#: views/notifications.tpl:136 msgid "Pushover" msgstr "" -#: views/notifications.tpl:107 +#: views/notifications.tpl:146 msgid "API Token" msgstr "" -#: views/notifications.tpl:111 +#: views/notifications.tpl:150 msgid "User key" msgstr "" -#: views/notifications.tpl:122 +#: views/notifications.tpl:161 msgid "Telegram" msgstr "" -#: views/notifications.tpl:132 +#: views/notifications.tpl:171 msgid "Bot Token" msgstr "" -#: views/notifications.tpl:136 +#: views/notifications.tpl:175 msgid "Username" msgstr "" -#: views/notifications.tpl:140 +#: views/notifications.tpl:179 msgid "Proxy" msgstr "" -#: views/notifications.tpl:151 +#: views/notifications.tpl:190 msgid "Messages" msgstr "" -#: views/notifications.tpl:162 +#: views/notifications.tpl:201 msgid "Trigger" msgstr "" -#: views/notifications.tpl:168 +#: views/notifications.tpl:207 msgid "Message" msgstr "" -#: views/notifications.tpl:171 +#: views/notifications.tpl:210 msgid "Service" msgstr "" diff --git a/locales/update_translations.sh b/locales/update_translations.sh index 5f7d9b9cc..fa989e3f2 100755 --- a/locales/update_translations.sh +++ b/locales/update_translations.sh @@ -9,7 +9,7 @@ cd - for translation in `grep -r -h -o -e "_('[^)]\+')" ../views/*.tpl ../views/inc/*.tpl ../static/js/terrariumpi.js ../*.py | sort | uniq | sed "s/\\\\\'/\\'/g" | sed "s/ /%20/g" `; do translation=${translation:3:-2} translation=${translation//\%20/ } - if [ `grep -c "\"${translation}\"" terrariumpi.pot` -eq 0 ]; then + if [ `grep -c -F "\"${translation}\"" terrariumpi.pot` -eq 0 ]; then echo "Adding missing ${translation}" echo "#: Missing text string" >> terrariumpi.pot echo "msgid \"${translation}\"" >> terrariumpi.pot diff --git a/terrariumEngine.py b/terrariumEngine.py index 7929f0da3..73c34e8d3 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -11,6 +11,7 @@ import thread import time +import datetime import uptime import os import psutil @@ -437,6 +438,13 @@ def __engine_loop(self): except Exception, err: print err + lcd_message = [datetime.datetime.now().strftime('%c')] + for env_part in average_data: + if 'light' not in env_part: + lcd_message.append('%s %.2f%s' % (_(env_part.replace('average_','').title()), average_data[env_part]['current'],average_data[env_part]['indicator'])) + + self.notification.send_lcd(lcd_message) + duration = (time.time() - starttime) + time_short if duration < terrariumEngine.LOOP_TIMEOUT: logger.info('Update done in %.5f seconds. Waiting for %.5f seconds for next update' % (duration,terrariumEngine.LOOP_TIMEOUT - duration)) diff --git a/terrariumLCD.py b/terrariumLCD.py new file mode 100644 index 000000000..8e00e0933 --- /dev/null +++ b/terrariumLCD.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# Source: https://gist.github.com/DenisFromHR/cc863375a6e19dce359d + +# -*- coding: utf-8 -*- +""" +Compiled, mashed and generally mutilated 2014-2015 by Denis Pleic +Made available under GNU GENERAL PUBLIC LICENSE + +# Modified Python I2C library for Raspberry Pi +# as found on http://www.recantha.co.uk/blog/?p=4849 +# Joined existing 'i2c_lib.py' and 'lcddriver.py' into a single library +# added bits and pieces from various sources +# By DenisFromHR (Denis Pleic) +# 2015-02-10, ver 0.1 + +""" + +import smbus +import time +import datetime +import thread + +from terrariumUtils import terrariumUtils,terrariumSingleton + +from gevent import monkey, sleep +monkey.patch_all() + +class i2c_device: + def __init__(self, addr, port=1): + self.addr = addr + self.bus = smbus.SMBus(port) + +# Write a single command + def write_cmd(self, cmd): + self.bus.write_byte(self.addr, cmd) + sleep(0.0001) + +# Write a command and argument + def write_cmd_arg(self, cmd, data): + self.bus.write_byte_data(self.addr, cmd, data) + sleep(0.0001) + +# Write a block of data + def write_block_data(self, cmd, data): + self.bus.write_block_data(self.addr, cmd, data) + sleep(0.0001) + +# Read a single byte + def read(self): + return self.bus.read_byte(self.addr) + +# Read + def read_data(self, cmd): + return self.bus.read_byte_data(self.addr, cmd) + +# Read a block of data + def read_block_data(self, cmd): + return self.bus.read_block_data(self.addr, cmd) + +class lcd: + # commands + LCD_CLEARDISPLAY = 0x01 + LCD_RETURNHOME = 0x02 + LCD_ENTRYMODESET = 0x04 + LCD_DISPLAYCONTROL = 0x08 + LCD_CURSORSHIFT = 0x10 + LCD_FUNCTIONSET = 0x20 + LCD_SETCGRAMADDR = 0x40 + LCD_SETDDRAMADDR = 0x80 + + # flags for display entry mode + LCD_ENTRYRIGHT = 0x00 + LCD_ENTRYLEFT = 0x02 + LCD_ENTRYSHIFTINCREMENT = 0x01 + LCD_ENTRYSHIFTDECREMENT = 0x00 + + # flags for display on/off control + LCD_DISPLAYON = 0x04 + LCD_DISPLAYOFF = 0x00 + LCD_CURSORON = 0x02 + LCD_CURSOROFF = 0x00 + LCD_BLINKON = 0x01 + LCD_BLINKOFF = 0x00 + + # flags for display/cursor shift + LCD_DISPLAYMOVE = 0x08 + LCD_CURSORMOVE = 0x00 + LCD_MOVERIGHT = 0x04 + LCD_MOVELEFT = 0x00 + + # flags for function set + LCD_8BITMODE = 0x10 + LCD_4BITMODE = 0x00 + LCD_2LINE = 0x08 + LCD_1LINE = 0x00 + LCD_5x10DOTS = 0x04 + LCD_5x8DOTS = 0x00 + + # flags for backlight control + LCD_BACKLIGHT = 0x08 + LCD_NOBACKLIGHT = 0x00 + + En = 0b00000100 # Enable bit + Rw = 0b00000010 # Read/Write bit + Rs = 0b00000001 # Register select bit + + #initializes objects and lcd + def __init__(self,address,device=1): + self.lcd_device = i2c_device(address,device) + + self.lcd_write(0x03) + self.lcd_write(0x03) + self.lcd_write(0x03) + self.lcd_write(0x02) + + self.lcd_write(lcd.LCD_FUNCTIONSET | lcd.LCD_2LINE | lcd.LCD_5x8DOTS | lcd.LCD_4BITMODE) + self.lcd_write(lcd.LCD_DISPLAYCONTROL | lcd.LCD_DISPLAYON) + self.lcd_write(lcd.LCD_CLEARDISPLAY) + self.lcd_write(lcd.LCD_ENTRYMODESET | lcd.LCD_ENTRYLEFT) + sleep(0.2) + + # clocks EN to latch command + def lcd_strobe(self, data): + self.lcd_device.write_cmd(data | lcd.En | lcd.LCD_BACKLIGHT) + sleep(.0005) + self.lcd_device.write_cmd(((data & ~lcd.En) | lcd.LCD_BACKLIGHT)) + sleep(.0001) + + def lcd_write_four_bits(self, data): + self.lcd_device.write_cmd(data | lcd.LCD_BACKLIGHT) + self.lcd_strobe(data) + + # write a command to lcd + def lcd_write(self, cmd, mode=0): + self.lcd_write_four_bits(mode | (cmd & 0xF0)) + self.lcd_write_four_bits(mode | ((cmd << 4) & 0xF0)) + + # write a character to lcd (or character rom) 0x09: backlight | RS=DR< + # works! + def lcd_write_char(self, charvalue, mode=1): + self.lcd_write_four_bits(mode | (charvalue & 0xF0)) + self.lcd_write_four_bits(mode | ((charvalue << 4) & 0xF0)) + + # put string function with optional char positioning + def lcd_display_string(self, string, line=1, pos=0): + if line == 1: + pos_new = pos + elif line == 2: + pos_new = 0x40 + pos + elif line == 3: + pos_new = 0x14 + pos + elif line == 4: + pos_new = 0x54 + pos + + self.lcd_write(0x80 + pos_new) + + for char in string: + self.lcd_write(ord(char), lcd.Rs) + + # clear lcd and set to home + def lcd_clear(self): + self.lcd_write(lcd.LCD_CLEARDISPLAY) + self.lcd_write(lcd.LCD_RETURNHOME) + + # define backlight on/off (lcd.backlight(1); off= lcd.backlight(0) + def backlight(self, state): # for state, 1 = on, 0 = off + if state == 1: + self.lcd_device.write_cmd(lcd.LCD_BACKLIGHT) + elif state == 0: + self.lcd_device.write_cmd(lcd.LCD_NOBACKLIGHT) + + # add custom characters (0 - 7) + def lcd_load_custom_chars(self, fontdata): + self.lcd_write(0x40); + for char in fontdata: + for line in char: + self.lcd_write_char(line) + +class terrariumLCD(): + __metaclass__ = terrariumSingleton + + def __init__(self,address,resolution = '16x2',title = False): + self.__address = None + self.__lcd = None + self.__title = False + + self.set_address(address) + self.set_resolution(resolution) + self.set_title(title) + + self.__text_animation = False + self.__rotation_timeout = 10 + self.__messages = [datetime.datetime.now().strftime('%c'),'Starting terrariumPI...'] + thread.start_new_thread(self.__rotate_messages, ()) + + def __rotate_messages(self): + max_chars = int(self.__resolution[0]) + max_lines = int(self.__resolution[1]) + + while True: + starttime = time.time() + messages = [message for message in self.__messages] + + for messagenr in xrange(0,len(messages)): + if messagenr >= max_lines: + timeout = float(self.__rotation_timeout) - (time.time() - starttime) + if timeout >= 0.0: + sleep(timeout) + starttime = time.time() + + for linenr in xrange(max_lines - (1 + (1 if self.__title else 0)),0,-1): + self.__animate_text(messages[messagenr - linenr], max_lines - linenr) + + self.__animate_text(messages[messagenr],max_lines) + + else: + self.__animate_text(messages[messagenr],(messagenr % max_lines) + 1) + + timeout = float(self.__rotation_timeout) - (time.time() - starttime) + if timeout >= 0.0: + sleep(timeout) + starttime = time.time() + + def __animate_text(self,message,linenr): + max_chars = int(self.__resolution[0]) + if self.__lcd is None: + return + + self.__lcd.lcd_display_string(message[:max_chars].ljust(max_chars),linenr) + + if len(message) > max_chars and not self.__text_animation: + self.__text_animation = True + sleep(0.2) + for counter in xrange(1,len(message)-max_chars): + self.__lcd.lcd_display_string(message[counter:max_chars+counter],linenr) + sleep(0.2) + + for counter in xrange(len(message)-max_chars,0,-1): + self.__lcd.lcd_display_string(message[counter:max_chars+counter],linenr) + sleep(0.2) + + self.__lcd.lcd_display_string(message[:max_chars].ljust(max_chars),linenr) + self.__text_animation = False + + def set_address(self,address): + self.__address = None + if address is not None and '' != address: + self.__address = address + address = address.split(',') + bus = 1 if len(address) == 1 else int(address[1]) + address = int('0x' + str(address[0]),16) + self.__lcd = lcd(address,bus) + + def get_address(self): + return self.__address + + def set_resolution(self,resolution): + self.__resolution = None + if resolution is not None and '' != resolution: + self.__resolution = resolution.split('x') + self.__resolution[0] = self.__resolution[0] + self.__resolution[1] = self.__resolution[1] + + def get_resolution(self): + if self.__resolution is not None: + return 'x'.join(self.__resolution) + + return '' + + def set_title(self,value): + self.__title = terrariumUtils.is_true(value) + + def get_title(self): + return self.__title == True + + def message(self,message): + if isinstance(message,basestring): + self.__messages = [message] + self.__lcd.lcd_display_string(message) + else: + self.__messages = [msg for msg in message] + + def get_config(self): + data = {'address' : self.get_address(), + 'resolution' : self.get_resolution(), + 'title' : self.get_title()} + + return data diff --git a/terrariumNotification.py b/terrariumNotification.py index c3019e90d..0554cc8fd 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -23,18 +23,12 @@ # Pushover support import pushover -from terrariumUtils import terrariumUtils +from terrariumUtils import terrariumUtils, terrariumSingleton +from terrariumLCD import terrariumLCD from gevent import monkey, sleep monkey.patch_all() -class Singleton(type): - _instances = {} - def __call__(cls, *args, **kwargs): - if cls not in cls._instances: - cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) - return cls._instances[cls] - class terrariumNotificationMessage(object): def __init__(self,message_id, title, message, services = ''): @@ -68,6 +62,9 @@ def is_pushover_enabled(self): def is_telegram_enabled(self): return self.message != '' and 'telegram' in self.services + def is_lcd_enabled(self): + return self.message != '' and 'lcd' in self.services + def get_data(self): return {'id':self.get_id(), 'title':self.get_title(), @@ -77,7 +74,7 @@ def get_data(self): } class terrariumNotificationTelegramBot(threading.Thread): - __metaclass__ = Singleton + __metaclass__ = terrariumSingleton __POLL_TIMEOUT = 120 @@ -259,8 +256,7 @@ def __init__(self,trafficlights = [], profile_image = None): self.twitter = None self.pushover = None self.telegram = None - - self.set_profile_image(profile_image) + self.lcd = None self.__load_config() self.__load_messages() @@ -268,6 +264,8 @@ def __init__(self,trafficlights = [], profile_image = None): if trafficlights is not None and len(trafficlights) == 3: self.set_notification_leds(trafficlights[0],trafficlights[1],trafficlights[2]) + self.set_profile_image(profile_image) + def __current_minute(self): # Get timestamp of current minute with 00 seconds. now = int(datetime.datetime.now().strftime('%s')) @@ -316,6 +314,11 @@ def __load_config(self): self.__data.get('telegram','userid'), proxy) + if self.__data.has_section('lcd'): + self.set_lcd(self.__data.get('lcd','address'), + self.__data.get('lcd','resolution'), + self.__data.get('lcd','title')) + def __load_messages(self,data = None): self.messages = {} for message_id in self.__default_notifications: @@ -541,7 +544,6 @@ def update_twitter_profile_image(self): except Exception, ex: print ex - def send_tweet(self,message): if self.twitter is None: return @@ -588,6 +590,19 @@ def send_telegram(self,subject,message): self.telegram.send_message(message) + def set_lcd(self,address,resolution,title): + if address is not None and '' != address: + if self.lcd is None: + self.lcd = terrariumLCD(address,resolution,title) + else: + self.lcd.set_address(address) + self.lcd.set_resolution(resolution) + self.lcd.set_title(title) + + def send_lcd(self,messages): + if self.lcd is not None: + self.lcd.message(messages) + def message(self,message_id,data = None): self.send_notication_led(message_id) @@ -628,6 +643,9 @@ def message(self,message_id,data = None): if self.messages[message_id].is_telegram_enabled(): self.send_telegram(title,message) + if self.messages[message_id].is_lcd_enabled(): + self.send_lcd(message) + def get_messages(self): data = [] for message_id in sorted(self.messages.keys()): @@ -655,6 +673,10 @@ def set_config(self,data): 'userid' : data['telegram_userid'], 'proxy' : data['telegram_proxy']}) + self.__update_config('lcd',{'address' : data['lcd_address'], + 'resolution' : data['lcd_resolution'], + 'title' : data['lcd_title']}) + except Exception, ex: print ex @@ -681,8 +703,9 @@ def set_config(self,data): def get_config(self): data = { - 'email' : dict(self.email) if self.email is not None else {}, - 'twitter' : dict(self.twitter) if self.twitter is not None else {}, + 'email' : dict(self.email) if self.email is not None else {}, + 'lcd' : self.lcd.get_config() if self.lcd is not None else {}, + 'twitter' : dict(self.twitter) if self.twitter is not None else {}, 'pushover' : dict(self.pushover) if self.pushover is not None else {}, 'telegram' : self.telegram.get_config() if self.telegram is not None else {}, 'messages' : self.get_messages() } diff --git a/terrariumTranslations.py b/terrariumTranslations.py index 6b29f5e9a..a0d9b1c72 100644 --- a/terrariumTranslations.py +++ b/terrariumTranslations.py @@ -109,6 +109,10 @@ def __load(self): self.translations['notification_email_email_username'] = _('Holds the username for authentication with the mailserver if needed.') self.translations['notification_email_email_password'] = _('Holds the password for authentication with the mailserver if needed.') + self.translations['notification_lcd_address'] = _('Holds the I2C address of the LCD screen. Use the value found with i2cdetect. Add ,[NR] to change the I2C bus.') + self.translations['notification_lcd_resolution'] = _('Holds the LCD screen resolution.') + self.translations['notification_lcd_title'] = _('Reserve first LCD line for static title.') + self.translations['notification_twitter_consumer_key'] = _('Holds your Twitter consumer key. More information %shere%s') % ('','') self.translations['notification_twitter_consumer_secret'] = _('Holds your Twitter consumer secret. More information %shere%s') % ('','') self.translations['notification_twitter_access_token'] = _('Holds your Twitter access token. More information %shere%s') % ('','') diff --git a/terrariumUtils.py b/terrariumUtils.py index 92fd59bed..2aaff4aec 100644 --- a/terrariumUtils.py +++ b/terrariumUtils.py @@ -235,3 +235,10 @@ def flatten_dict(dd, separator='_', prefix=''): for kk, vv in dd.items() for k, v in terrariumUtils.flatten_dict(vv, separator, kk).items() } if isinstance(dd, dict) else { prefix : dd if not isinstance(dd,list) else ','.join(dd)} + +class terrariumSingleton(type): + _instances = {} + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(terrariumSingleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] diff --git a/views/notifications.tpl b/views/notifications.tpl index cc9a97362..fe40f3517 100644 --- a/views/notifications.tpl +++ b/views/notifications.tpl @@ -57,6 +57,45 @@ +
+
+
+
+

{{_('LCD')}} {{_('Settings')}}

+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
@@ -241,6 +280,12 @@ toggleService(this.id) }); + $("select").select2({ + placeholder: '{{_('Select an option')}}', + allowClear: false, + minimumResultsForSearch: Infinity + }); + $('div#notifications_telegram input[name="telegram_bot_token"]').on('change',function() { if (this.value != '') { $.get('https://api.telegram.org/bot' + this.value + '/getMe', function(data){ @@ -266,6 +311,7 @@ $.each(data.notifications,function(part,partdata) { switch (part) { case 'email': + case 'lcd': case 'twitter': case 'pushover': case 'telegram': @@ -274,7 +320,9 @@ if (config_field.length >= 1) { switch (config_field.prop('type').toLowerCase()) { case 'text': - config_field.val(value).trigger('change'); + case 'select-one': + case 'select-multiple': + config_field.val(value + '').trigger('change'); break; } } From f86de9afbd1324712d401f8de283debe3c82e7b6 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 20:26:48 +0200 Subject: [PATCH 23/33] Move timestamp to LCD code --- terrariumEngine.py | 2 +- terrariumLCD.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/terrariumEngine.py b/terrariumEngine.py index 73c34e8d3..b90d3376d 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -438,7 +438,7 @@ def __engine_loop(self): except Exception, err: print err - lcd_message = [datetime.datetime.now().strftime('%c')] + lcd_message = [] for env_part in average_data: if 'light' not in env_part: lcd_message.append('%s %.2f%s' % (_(env_part.replace('average_','').title()), average_data[env_part]['current'],average_data[env_part]['indicator'])) diff --git a/terrariumLCD.py b/terrariumLCD.py index 8e00e0933..767b6002d 100644 --- a/terrariumLCD.py +++ b/terrariumLCD.py @@ -190,7 +190,7 @@ def __init__(self,address,resolution = '16x2',title = False): self.__text_animation = False self.__rotation_timeout = 10 - self.__messages = [datetime.datetime.now().strftime('%c'),'Starting terrariumPI...'] + self.__messages = ['Starting terrariumPI...'] thread.start_new_thread(self.__rotate_messages, ()) def __rotate_messages(self): @@ -200,6 +200,7 @@ def __rotate_messages(self): while True: starttime = time.time() messages = [message for message in self.__messages] + messages.insert(0,datetime.datetime.now().strftime('%c')) for messagenr in xrange(0,len(messages)): if messagenr >= max_lines: From 5d0fe42f78c8879556f6b991928cfabd1d1beae8 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 22:22:09 +0200 Subject: [PATCH 24/33] Rewriting getting remote data. Trying to fix proxy issues with Telegram. #161 --- terrariumLCD.py | 1 - terrariumNotification.py | 30 ++++++---- terrariumUtils.py | 36 +++++++----- terrariumWeather.py | 115 +++++++++++++++++++++++---------------- 4 files changed, 109 insertions(+), 73 deletions(-) diff --git a/terrariumLCD.py b/terrariumLCD.py index 767b6002d..760c39f17 100644 --- a/terrariumLCD.py +++ b/terrariumLCD.py @@ -194,7 +194,6 @@ def __init__(self,address,resolution = '16x2',title = False): thread.start_new_thread(self.__rotate_messages, ()) def __rotate_messages(self): - max_chars = int(self.__resolution[0]) max_lines = int(self.__resolution[1]) while True: diff --git a/terrariumNotification.py b/terrariumNotification.py index 0554cc8fd..5c67b64ab 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -81,6 +81,8 @@ class terrariumNotificationTelegramBot(threading.Thread): def __init__(self,bot_token,valid_users = None, proxy = None): super(terrariumNotificationTelegramBot,self).__init__() self.__running = False + self.__proxy = None + self.__bot_token = bot_token self.__bot_url = 'https://api.telegram.org/bot{}/'.format(self.__bot_token) self.__chat_ids = [] @@ -91,7 +93,7 @@ def __init__(self,bot_token,valid_users = None, proxy = None): self.set_proxy(proxy) self.start() - def __get_url(self,url): + '''def __get_url(self,url): data = '' try: if self.__proxy is not None: @@ -103,17 +105,18 @@ def __get_url(self,url): except Exception, ex: print ex - return data + return data''' - def __get_json_from_url(self,url): + '''def __get_json_from_url(self,url): data = {'description' : 'Did not receive valid JSON data'} try: - content = self.__get_url(url) - data = json.loads(content) + #content = self.__get_url(url) + data = + #data = json.loads(content) except Exception, ex: print ex - return data + return data''' def __get_updates(self,offset=None): self.__last_update_check = int(time.time()) @@ -121,7 +124,11 @@ def __get_updates(self,offset=None): if offset: url += '&offset={}'.format(offset) - return self.__get_json_from_url(url) + data = terrariumUtils.get_remote_data(url,terrariumNotificationTelegramBot.__POLL_TIMEOUT + 3,proxy=self.__proxy) + if data is None: + data = {'description' : 'Did not receive valid JSON data'} + + return data def __process_messages(self,messages): for update in messages: @@ -141,7 +148,7 @@ def __process_messages(self,messages): def get_config(self): return {'bot_token' : self.__bot_token, 'userid': ','.join(self.__valid_users) if self.__valid_users is not None else '', - 'proxy' : self.__proxy['https'] if self.__proxy is not None else None} + 'proxy' : self.__proxy if self.__proxy is not None else None} def send_message(self,text, chat_id = None): if self.__running: @@ -149,7 +156,9 @@ def send_message(self,text, chat_id = None): text = urllib.quote_plus(text) for chat_id in chat_ids: url = self.__bot_url + 'sendMessage?text={}&chat_id={}'.format(text, chat_id) - self.__get_url(url) + terrariumUtils.get_remote_data(url,proxy=self.__proxy) + + #self.__get_url(url) def set_valid_users(self,users = None): self.__valid_users = users.split(',') if users is not None else [] @@ -157,8 +166,7 @@ def set_valid_users(self,users = None): def set_proxy(self,proxy): self.__proxy = None if proxy is not None and '' != proxy: - self.__proxy = {'http' : proxy, - 'https': proxy} + self.__proxy = proxy def stop(self): self.__running = False diff --git a/terrariumUtils.py b/terrariumUtils.py index 2aaff4aec..5dda01319 100644 --- a/terrariumUtils.py +++ b/terrariumUtils.py @@ -21,13 +21,13 @@ def to_inches(value): @staticmethod def is_float(value): - if value is None: + if value is None or '' == value: return False try: float(value) return True - except ValueError: + except Exception: return False @staticmethod @@ -137,24 +137,32 @@ def parse_time(value): return time @staticmethod - def get_remote_data(url): + def get_remote_data(url, timeout = 3, proxy = None): data = None try: url_data = terrariumUtils.parse_url(url) - data = requests.get(url,auth=(url_data['username'],url_data['password']),timeout=3) + proxies = {'http' : proxy, 'https' : proxy} - if data.status_code == 200: - data = data.json() - json_path = url_data['fragment'].split('/') if 'fragment' in url_data and url_data['fragment'] is not None else [] + print 'Get data from %s' % url + print proxies - for item in json_path: - # Dirty hack to process array data.... - try: - item = int(item) - except Exception, ex: - item = str(item) + request = requests.get(url,auth=(url_data['username'],url_data['password']),timeout=timeout,proxies=proxies) + + if request.status_code == 200: + if 'application/json' in request.headers['content-type']: + data = request.json() + json_path = url_data['fragment'].split('/') if 'fragment' in url_data and url_data['fragment'] is not None else [] + for item in json_path: + # Dirty hack to process array data.... + try: + item = int(item) + except Exception, ex: + item = str(item) + + data = data[item] + else: + return request.content.decode('utf8') - data = data[item] else: data = None diff --git a/terrariumWeather.py b/terrariumWeather.py index c1465ce5b..ac6e5e501 100644 --- a/terrariumWeather.py +++ b/terrariumWeather.py @@ -77,13 +77,22 @@ def get_forecast(self,period = 'day'): class terrariumWeatherYRno(terrariumWeatherSource): def load_data(self): + self.sunrise = int(datetime.now().replace(hour=8, minute=0, second=0).strftime('%s')) + self.sunset = self.sunrise + (12 * 60 * 60) + starttime = time.time() logger.info('Update YR.no data from ONLINE refreshing cache.') self.type = 'yr.no' - try: + xmldata = terrariumUtils.get_remote_data(self.source_url.strip('/') + '/forecast_hour_by_hour.xml') + if xmldata is not None: + try: + xmldata = untangle.parse(xmldata) + except Exception: + logger.exception('Error getting online data from yr.no') + return False + # Parse hour forecast - xmldata = untangle.parse(self.source_url.strip('/') + '/forecast_hour_by_hour.xml') # Parse general data information self.city = xmldata.weatherdata.location.name.cdata self.country = xmldata.weatherdata.location.country.cdata @@ -109,20 +118,30 @@ def load_data(self): # Parse week forecast self.week_forecast = [] - xmldata = untangle.parse(self.source_url.strip('/') + '/forecast.xml') - for forecast in xmldata.weatherdata.forecast.tabular.time: - self.week_forecast.append({ 'from' : time.mktime(dateutil.parser.parse(forecast['from']).timetuple()), - 'to' : time.mktime(dateutil.parser.parse(forecast['to']).timetuple()), - 'weather' : forecast.symbol['name'], - 'rain' : float(forecast.precipitation['value']), - 'humidity' : 0, - 'wind_direction' : forecast.windDirection['name'], - 'wind_speed' : float(forecast.windSpeed['mps']), - 'temperature' : float(forecast.temperature['value']), - 'pressure' : float(forecast.pressure['value']) - }) - except Exception: - logger.exception('Error getting online data from yr.no') + xmldata = terrariumUtils.get_remote_data(self.source_url.strip('/') + '/forecast.xml') + if xmldata is not None: + try: + xmldata = untangle.parse(xmldata) + except Exception: + logger.exception('Error getting online data from yr.no') + + + #xmldata = untangle.parse(self.source_url.strip('/') + '/forecast.xml') + for forecast in xmldata.weatherdata.forecast.tabular.time: + self.week_forecast.append({ 'from' : time.mktime(dateutil.parser.parse(forecast['from']).timetuple()), + 'to' : time.mktime(dateutil.parser.parse(forecast['to']).timetuple()), + 'weather' : forecast.symbol['name'], + 'rain' : float(forecast.precipitation['value']), + 'humidity' : 0, + 'wind_direction' : forecast.windDirection['name'], + 'wind_speed' : float(forecast.windSpeed['mps']), + 'temperature' : float(forecast.temperature['value']), + 'pressure' : float(forecast.pressure['value']) + }) + else: + logger.error('Error getting online data from yr.no') + else: + logger.error('Error getting online data from yr.no') return False return True @@ -130,14 +149,15 @@ def load_data(self): class terrariumWeatherWunderground(terrariumWeatherSource): def load_data(self): + self.sunrise = int(datetime.now().replace(hour=8, minute=0, second=0).strftime('%s')) + self.sunset = self.sunrise + (12 * 60 * 60) + logger.info('Update Wunderground data from ONLINE refreshing cache.') self.type = 'weather.com' self.copyright = {'text' : 'Wunderground weather data', 'url' : ''} - try: - json_data = urllib2.urlopen(self.source_url) - parsed_json = json.loads(json_data.read()) - + parsed_json = terrariumUtils.get_remote_data(self.source_url) + if parsed_json is not None: # Parse general data information self.city = parsed_json['location']['city'] self.country = parsed_json['location']['country_name'] @@ -174,8 +194,8 @@ def load_data(self): if forecast_hour['to'] <= datelimit: self.hour_forecast.append(copy.deepcopy(forecast_hour)) - except Exception: - logger.exception('Error getting online data from weather.com') + else: + logger.error('Error getting online data from weather.com') return False return True @@ -183,14 +203,15 @@ def load_data(self): class terrariumWeatherOpenWeathermap(terrariumWeatherSource): def load_data(self): + self.sunrise = int(datetime.now().replace(hour=8, minute=0, second=0).strftime('%s')) + self.sunset = self.sunrise + (12 * 60 * 60) + logger.info('Update OpenWeatherMap data from ONLINE refreshing cache.') self.type = 'openweathermap.org' self.copyright = {'text' : 'OpenWeatherMap data', 'url' : 'https://openweathermap.org/city/'} - try: - json_data = urllib2.urlopen(self.source_url) - parsed_json = json.loads(json_data.read()) - + parsed_json = terrariumUtils.get_remote_data(self.source_url) + if parsed_json is not None: # Parse general data information self.city = parsed_json['name'] self.country = parsed_json['sys']['country'] @@ -200,30 +221,30 @@ def load_data(self): self.sunrise = parsed_json['sys']['sunrise'] self.sunset = parsed_json['sys']['sunset'] - # Parse hourly and week forecast - json_data = urllib2.urlopen(self.source_url.replace('/weather?q','/forecast?q')) - parsed_json = json.loads(json_data.read()) - self.hour_forecast = [] self.week_forecast = [] - datelimit = int(time.time()) + (2 * 24 * 60 * 60) # Hourly forecast limit of 2 days - for forecast in parsed_json['list']: - forecast_hour = { 'from' : forecast['dt'], - 'to' : forecast['dt'] + (3 * 60 * 60), # Data is provided per 3 hours - 'weather' : forecast['weather'][0]['description'], - 'rain' : (float(forecast['rain']['3h']) / 3.0) if '3h' in forecast['rain'] else 0, # Guess in mm - 'humidity' : float(forecast['main']['humidity']), - 'wind_direction' : forecast['wind']['deg'], - 'wind_speed' : float(forecast['wind']['speed']) / 3.6, - 'temperature' : float(forecast['main']['temp']), - 'pressure' : float(forecast['main']['pressure']) - } - self.week_forecast.append(copy.deepcopy(forecast_hour)) - if forecast_hour['to'] <= datelimit: - self.hour_forecast.append(copy.deepcopy(forecast_hour)) - except Exception: - logger.exception('Error getting online data from openweathermap.org') + parsed_json = terrariumUtils.get_remote_data(self.source_url.replace('/weather?q','/forecast?q')) + if parsed_json is not None: + # Parse hourly and week forecast + datelimit = int(time.time()) + (2 * 24 * 60 * 60) # Hourly forecast limit of 2 days + for forecast in parsed_json['list']: + forecast_hour = { 'from' : forecast['dt'], + 'to' : forecast['dt'] + (3 * 60 * 60), # Data is provided per 3 hours + 'weather' : forecast['weather'][0]['description'], + 'rain' : (float(forecast['rain']['3h']) / 3.0) if 'rain' in forecast and '3h' in forecast['rain'] else 0, # Guess in mm + 'humidity' : float(forecast['main']['humidity']), + 'wind_direction' : forecast['wind']['deg'], + 'wind_speed' : float(forecast['wind']['speed']) / 3.6, + 'temperature' : float(forecast['main']['temp']), + 'pressure' : float(forecast['main']['pressure']) + } + self.week_forecast.append(copy.deepcopy(forecast_hour)) + if forecast_hour['to'] <= datelimit: + self.hour_forecast.append(copy.deepcopy(forecast_hour)) + + else: + logger.error('Error getting online data from openweathermap.org') return False return True From 812d1cc6fe6e5196e7f37cac0f0039cf7560e49a Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 22:32:34 +0200 Subject: [PATCH 25/33] Code cleanup --- terrariumNotification.py | 29 +---------------------------- terrariumUtils.py | 14 +++++--------- 2 files changed, 6 insertions(+), 37 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 5c67b64ab..0be7e6fda 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -93,31 +93,6 @@ def __init__(self,bot_token,valid_users = None, proxy = None): self.set_proxy(proxy) self.start() - '''def __get_url(self,url): - data = '' - try: - if self.__proxy is not None: - response = requests.get(url,proxies=self.__proxy) - else: - response = requests.get(url) - - data = response.content.decode('utf8') - except Exception, ex: - print ex - - return data''' - - '''def __get_json_from_url(self,url): - data = {'description' : 'Did not receive valid JSON data'} - try: - #content = self.__get_url(url) - data = - #data = json.loads(content) - except Exception, ex: - print ex - - return data''' - def __get_updates(self,offset=None): self.__last_update_check = int(time.time()) url = self.__bot_url + 'getUpdates?timeout={}'.format(terrariumNotificationTelegramBot.__POLL_TIMEOUT) @@ -148,7 +123,7 @@ def __process_messages(self,messages): def get_config(self): return {'bot_token' : self.__bot_token, 'userid': ','.join(self.__valid_users) if self.__valid_users is not None else '', - 'proxy' : self.__proxy if self.__proxy is not None else None} + 'proxy' : self.__proxy if self.__proxy is not None else ''} def send_message(self,text, chat_id = None): if self.__running: @@ -158,8 +133,6 @@ def send_message(self,text, chat_id = None): url = self.__bot_url + 'sendMessage?text={}&chat_id={}'.format(text, chat_id) terrariumUtils.get_remote_data(url,proxy=self.__proxy) - #self.__get_url(url) - def set_valid_users(self,users = None): self.__valid_users = users.split(',') if users is not None else [] diff --git a/terrariumUtils.py b/terrariumUtils.py index 5dda01319..0776c7f38 100644 --- a/terrariumUtils.py +++ b/terrariumUtils.py @@ -142,15 +142,11 @@ def get_remote_data(url, timeout = 3, proxy = None): try: url_data = terrariumUtils.parse_url(url) proxies = {'http' : proxy, 'https' : proxy} + response = requests.get(url,auth=(url_data['username'],url_data['password']),timeout=timeout,proxies=proxies) - print 'Get data from %s' % url - print proxies - - request = requests.get(url,auth=(url_data['username'],url_data['password']),timeout=timeout,proxies=proxies) - - if request.status_code == 200: - if 'application/json' in request.headers['content-type']: - data = request.json() + if response.status_code == 200: + if 'application/json' in response.headers['content-type']: + data = response.json() json_path = url_data['fragment'].split('/') if 'fragment' in url_data and url_data['fragment'] is not None else [] for item in json_path: # Dirty hack to process array data.... @@ -161,7 +157,7 @@ def get_remote_data(url, timeout = 3, proxy = None): data = data[item] else: - return request.content.decode('utf8') + data = response.content.decode('utf8') else: data = None From 24fcce11b708b58d6d0ffaf9be3fe14df882065b Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 23:07:59 +0200 Subject: [PATCH 26/33] Fixing hanging Telegram Bot --- terrariumNotification.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 0be7e6fda..292c52b88 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -6,7 +6,7 @@ import time import os import os.path -import threading +import thread import json import requests import urllib @@ -73,13 +73,12 @@ def get_data(self): 'services' : ','.join(self.services) } -class terrariumNotificationTelegramBot(threading.Thread): +class terrariumNotificationTelegramBot(object): __metaclass__ = terrariumSingleton __POLL_TIMEOUT = 120 def __init__(self,bot_token,valid_users = None, proxy = None): - super(terrariumNotificationTelegramBot,self).__init__() self.__running = False self.__proxy = None @@ -91,7 +90,8 @@ def __init__(self,bot_token,valid_users = None, proxy = None): self.set_valid_users(valid_users) self.set_proxy(proxy) - self.start() + + thread.start_new_thread(self.__run, ()) def __get_updates(self,offset=None): self.__last_update_check = int(time.time()) @@ -145,7 +145,7 @@ def stop(self): self.__running = False print '%s - INFO - terrariumNotificatio - Stopping TelegramBot. This can take up to %s seconds...' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],(terrariumNotificationTelegramBot.__POLL_TIMEOUT - (int(time.time()) - self.__last_update_check))) - def run(self): + def __run(self): self.__running = True last_update_id = None @@ -159,12 +159,12 @@ def run(self): elif 'description' in updates: print updates print '%s - ERROR - terrariumNotificatio - TelegramBot has issues: %s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],updates['description']) - time.sleep(5) + sleep(5) - time.sleep(0.5) + sleep(0.5) except Exception, ex: print ex - time.sleep(5) + sleep(5) print '%s - INFO - terrariumNotificatio - TelegramBot is stopped' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],) From dab0d8406a5b2f29ac49f6262fa7e739451d1bb9 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 23:19:44 +0200 Subject: [PATCH 27/33] Stop after 2 errors --- terrariumNotification.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 292c52b88..4fe53da8b 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -149,20 +149,25 @@ def __run(self): self.__running = True last_update_id = None - while self.__running: + error_counter = 0 + + while self.__running and error_counter < 2: try: updates = self.__get_updates(last_update_id) if 'result' in updates and len(updates['result']) > 0: last_update_id = max([int(update['update_id']) for update in updates['result']]) + 1 self.__process_messages(updates['result']) + if error_counter > 0: + error_counter -= 1 elif 'description' in updates: - print updates + error_counter += 1 print '%s - ERROR - terrariumNotificatio - TelegramBot has issues: %s' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],updates['description']) sleep(5) sleep(0.5) except Exception, ex: + error_counter += 1 print ex sleep(5) From d20c3d275a901e12dc1510c3271716a74477c951 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 23:31:58 +0200 Subject: [PATCH 28/33] Restart Telegram Bot after changing settings if needed --- terrariumNotification.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/terrariumNotification.py b/terrariumNotification.py index 4fe53da8b..e8d2a5b1a 100644 --- a/terrariumNotification.py +++ b/terrariumNotification.py @@ -90,8 +90,7 @@ def __init__(self,bot_token,valid_users = None, proxy = None): self.set_valid_users(valid_users) self.set_proxy(proxy) - - thread.start_new_thread(self.__run, ()) + self.start() def __get_updates(self,offset=None): self.__last_update_check = int(time.time()) @@ -141,6 +140,10 @@ def set_proxy(self,proxy): if proxy is not None and '' != proxy: self.__proxy = proxy + def start(self): + if not self.__running: + thread.start_new_thread(self.__run, ()) + def stop(self): self.__running = False print '%s - INFO - terrariumNotificatio - Stopping TelegramBot. This can take up to %s seconds...' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],(terrariumNotificationTelegramBot.__POLL_TIMEOUT - (int(time.time()) - self.__last_update_check))) @@ -150,7 +153,6 @@ def __run(self): last_update_id = None error_counter = 0 - while self.__running and error_counter < 2: try: updates = self.__get_updates(last_update_id) @@ -171,6 +173,7 @@ def __run(self): print ex sleep(5) + self.__running = False print '%s - INFO - terrariumNotificatio - TelegramBot is stopped' % (datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:23],) class terrariumNotification(object): @@ -569,6 +572,7 @@ def set_telegram(self,bot_token,userid,proxy): else: self.telegram.set_valid_users(userid) self.telegram.set_proxy(proxy) + self.telegram.start() def send_telegram(self,subject,message): if self.telegram is None: From ed47172d14baf2178bfeb5e6030030ecb9ddd202 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sat, 30 Jun 2018 23:42:53 +0200 Subject: [PATCH 29/33] Auto decode HTML content --- terrariumUtils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terrariumUtils.py b/terrariumUtils.py index 0776c7f38..7316cacd0 100644 --- a/terrariumUtils.py +++ b/terrariumUtils.py @@ -157,7 +157,7 @@ def get_remote_data(url, timeout = 3, proxy = None): data = data[item] else: - data = response.content.decode('utf8') + data = response.text else: data = None From ceaeec075df8326a6faaf282c8cfe2885d54c30b Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 1 Jul 2018 00:05:36 +0200 Subject: [PATCH 30/33] Smoothen the dimmer --- terrariumSwitch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terrariumSwitch.py b/terrariumSwitch.py index f00bd409e..33670005a 100644 --- a/terrariumSwitch.py +++ b/terrariumSwitch.py @@ -31,8 +31,8 @@ class terrariumSwitch(object): # PWM Dimmer settings PWM_DIMMER_MAXDIM = 895 # http://www.esp8266-projects.com/2017/04/raspberry-pi-domoticz-ac-dimmer-part-1/ - PWM_DIMMER_MIN_TIMEOUT=0.2 - PWM_DIMMER_MIN_STEP=1 + PWM_DIMMER_MIN_TIMEOUT=0.1 + PWM_DIMMER_MIN_STEP=0.1 BITBANG_ADDRESSES = { "1":"2", From 1957ad6a477889002b1af850a46c87157d7a1ac3 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 1 Jul 2018 00:06:10 +0200 Subject: [PATCH 31/33] Update changelog --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7eebf25..6be945646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,69 @@ Changelog ========= +3.8.0 (2018-06-30) +------------------ + +**New** +------ +- Added support for LCD screens through notification system. [#164](https://github.com/theyosh/TerrariumPI/issues/164) [#101](https://github.com/theyosh/TerrariumPI/issues/101). + [theyosh] +- Add proxy support for Telegram. [#161](https://github.com/theyosh/TerrariumPI/issues/161). [theyosh] + +**Fixes** +------ +- Fixing hanging Telegram Bot. [theyosh] +- Rewriting getting remote data. Trying to fix proxy issues with + Telegram. [#161](https://github.com/theyosh/TerrariumPI/issues/161). [theyosh] +- Fix missing dimmer step setting. [theyosh] +- Fix database recovery. [theyosh] +- Fix environment status for manual power switch toggling. [theyosh] +- Better fix for tooltips in graphs. [theyosh] +- Fix tooltip HTML code. [theyosh] +- Fix telegram bot socks setting [#161](https://github.com/theyosh/TerrariumPI/issues/161). [theyosh] +- Fix total power usage (2) [theyosh] +- Fix total power usage. [theyosh] +- Fixing telegram bot to be more resistant to errors. [theyosh] + +**Updates** +------ +- Update README.md. [TheYOSH] +- Update translations. [theyosh] +- Small update to installer and reload message settings after saving. + [#101](https://github.com/theyosh/TerrariumPI/issues/101) [#161](https://github.com/theyosh/TerrariumPI/issues/161). [theyosh] +- Small update to installer and reload message settings after saving. + [#101](https://github.com/theyosh/TerrariumPI/issues/101) [#161](https://github.com/theyosh/TerrariumPI/issues/161). [theyosh] +- Update Telegram box proxy settings. [theyosh] +- Better and safer upgrade. [theyosh] +- Update version number. [theyosh] +- Updated data collector: - Removed duplicate data records for power + switches and doors - Added and changed indexes for faster quering + - Put more logic in queries and less in code. [theyosh] + + This will improve the overall query time with 50%. And improve the average query times with 400%!! + +**Other** +------ +- Smoothen the dimmer. [theyosh] +- Auto decode HTML content. [theyosh] +- Restart Telegram Bot after changing settings if needed. [theyosh] +- Stop after 2 errors. [theyosh] +- Code cleanup. [theyosh] +- Move timestamp to LCD code. [theyosh] +- Merge branch 'development' of ssh://github.com/theyosh/TerrariumPI + into development. [theyosh] +- Remove debig. [theyosh] +- Final collector code. And good looking graphs. [theyosh] +- Merge branch 'master' into development. [theyosh] +- Merge pull request [#162](https://github.com/theyosh/TerrariumPI/issues/162) from theyosh/development. [TheYOSH] + + Add proxy support for Telegram. [#161](https://github.com/theyosh/TerrariumPI/issues/161) +- Stash. [theyosh] +- Another attempt to get the powerswitches and door nicer graphs. + [theyosh] +- Change quotes. [theyosh] + + 3.7.0 (2018-06-20) ------------------ @@ -25,6 +88,7 @@ Changelog **Updates** ------ +- Update CHANGELOG. [theyosh] - Update version number. [theyosh] - Update twitter image based on profile image. [#101](https://github.com/theyosh/TerrariumPI/issues/101). [theyosh] - Update notification translations. [#101](https://github.com/theyosh/TerrariumPI/issues/101). [theyosh] @@ -40,6 +104,9 @@ Changelog **Other** ------ +- Merge pull request [#160](https://github.com/theyosh/TerrariumPI/issues/160) from theyosh/development. [TheYOSH] + + New release - Some cosmetic touchups... [#101](https://github.com/theyosh/TerrariumPI/issues/101). [theyosh] - Remove debug. [theyosh] - Typo. [theyosh] From fa60e8ef726124c56e20573c86ca7bd8e7983013 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 1 Jul 2018 14:20:09 +0200 Subject: [PATCH 32/33] Finetuning --- terrariumDoor.py | 5 ++++- terrariumEngine.py | 14 +++++++++----- terrariumLCD.py | 1 + terrariumUtils.py | 4 ++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/terrariumDoor.py b/terrariumDoor.py index 1e0d496a4..3a1c8dee4 100644 --- a/terrariumDoor.py +++ b/terrariumDoor.py @@ -8,6 +8,9 @@ from hashlib import md5 from terrariumUtils import terrariumUtils +from gevent import monkey, sleep +monkey.patch_all() + class terrariumDoor(object): VALID_HARDWARE_TYPES = ['gpio','remote'] CHECKER_TIMEOUT = 0.25 @@ -73,7 +76,7 @@ def __checker(self): if self.callback is not None: self.callback(self.get_data()) - time.sleep(terrariumDoor.CHECKER_TIMEOUT) + sleep(terrariumDoor.CHECKER_TIMEOUT) def get_data(self): return {'id': self.get_id(), diff --git a/terrariumEngine.py b/terrariumEngine.py index b90d3376d..c36e81ea8 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -72,7 +72,7 @@ def __init__(self): self.config = terrariumConfig() logger.info('Done Loading terrariumPI config') - # Notification engine + # Notification engine self.notification = terrariumNotification(profile_image = self.get_profile_image()) logger.info('Setting terrariumPI authentication') @@ -428,7 +428,8 @@ def __engine_loop(self): self.get_audio_playing(socket=True) # Log system stats - self.collector.log_system_data(self.get_system_stats()) + system_data = self.get_system_stats() + self.collector.log_system_data(system_data) self.get_system_stats(socket=True) for webcamid in self.webcams: @@ -438,10 +439,13 @@ def __engine_loop(self): except Exception, err: print err - lcd_message = [] + lcd_message = ['%s %s' % (_('Uptime'),terrariumUtils.format_uptime(system_data['uptime']),), + '%s %s %s %s' % (_('Load'),system_data['load']['load1'],system_data['load']['load5'],system_data['load']['load15']), + '%s %.2f%s' % (_('CPU Temp.'),system_data['temperature'],self.get_temperature_indicator())] + for env_part in average_data: - if 'light' not in env_part: - lcd_message.append('%s %.2f%s' % (_(env_part.replace('average_','').title()), average_data[env_part]['current'],average_data[env_part]['indicator'])) + alarm_icon = '!' if average_data[env_part]['alarm'] else '' + lcd_message.append('%s%s %.2f%s%s' % (alarm_icon,_(env_part.replace('average_','').title()), average_data[env_part]['current'],average_data[env_part]['indicator'],alarm_icon)) self.notification.send_lcd(lcd_message) diff --git a/terrariumLCD.py b/terrariumLCD.py index 760c39f17..1943c9f61 100644 --- a/terrariumLCD.py +++ b/terrariumLCD.py @@ -215,6 +215,7 @@ def __rotate_messages(self): else: self.__animate_text(messages[messagenr],(messagenr % max_lines) + 1) + starttime = time.time() timeout = float(self.__rotation_timeout) - (time.time() - starttime) if timeout >= 0.0: diff --git a/terrariumUtils.py b/terrariumUtils.py index 7316cacd0..188d31127 100644 --- a/terrariumUtils.py +++ b/terrariumUtils.py @@ -240,6 +240,10 @@ def flatten_dict(dd, separator='_', prefix=''): for k, v in terrariumUtils.flatten_dict(vv, separator, kk).items() } if isinstance(dd, dict) else { prefix : dd if not isinstance(dd,list) else ','.join(dd)} + @staticmethod + def format_uptime(value): + return str(datetime.timedelta(seconds=int(value))) + class terrariumSingleton(type): _instances = {} def __call__(cls, *args, **kwargs): From ef7e1cf4fe358d1210c47d8e4d114ff52d2d0935 Mon Sep 17 00:00:00 2001 From: theyosh Date: Sun, 1 Jul 2018 17:15:07 +0200 Subject: [PATCH 33/33] Update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be945646..88e5a8b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Changelog ========= -3.8.0 (2018-06-30) +3.8.0 (2018-07-01) ------------------ **New** @@ -28,6 +28,7 @@ Changelog **Updates** ------ +- Update changelog. [theyosh] - Update README.md. [TheYOSH] - Update translations. [theyosh] - Small update to installer and reload message settings after saving. @@ -45,6 +46,7 @@ Changelog **Other** ------ +- Finetuning. [theyosh] - Smoothen the dimmer. [theyosh] - Auto decode HTML content. [theyosh] - Restart Telegram Bot after changing settings if needed. [theyosh]