diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7eebf25..88e5a8b3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,71 @@ Changelog ========= +3.8.0 (2018-07-01) +------------------ + +**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 changelog. [theyosh] +- 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** +------ +- Finetuning. [theyosh] +- 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 +90,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 +106,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] 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. 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 diff --git a/locales/en_US/LC_MESSAGES/terrariumpi.mo b/locales/en_US/LC_MESSAGES/terrariumpi.mo index 9c39dac26..23e99cdf0 100644 Binary files a/locales/en_US/LC_MESSAGES/terrariumpi.mo and b/locales/en_US/LC_MESSAGES/terrariumpi.mo differ diff --git a/locales/en_US/LC_MESSAGES/terrariumpi.po b/locales/en_US/LC_MESSAGES/terrariumpi.po index c7e3651dd..80d6ecfa1 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-30 18:33+CEST\n" +"PO-Revision-Date: 2018-06-30 18:33+0200\n" "Last-Translator: Joshua (TheYOSH) Rubingh \n" "Language-Team: \n" "Language: en_US\n" @@ -328,99 +328,147 @@ 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: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:117 +msgid "Holds your Twitter consumer secret. More information %shere%s" +msgstr "Holds your Twitter consumer secret. More information %shere%s" + +#: 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: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:121 +msgid "Holds the PushOver API token. More information %shere%s" +msgstr "Holds the PushOver API token. More information %shere%s" + +#: 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:124 +msgid "Holds the Telegram Bot token. More information %shere%s" +msgstr "Holds the Telegram Bot token. More information %shere%s" + #: 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: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:130 msgid "Choose your interface language." msgstr "Choose your interface language." -#: terrariumTranslations.py:126 +#: terrariumTranslations.py:131 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:132 terrariumTranslations.py:133 msgid "Holds the username which can make changes (Administrator)." msgstr "Holds the username which can make changes (Administrator)." -#: terrariumTranslations.py:129 +#: 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:130 +#: 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:131 +#: 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:132 +#: terrariumTranslations.py:137 msgid "Holds the external calendar url." msgstr "Holds the external calendar url." -#: terrariumTranslations.py:133 +#: 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:134 +#: 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:135 +#: 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:136 +#: 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:137 +#: 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:138 +#: 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:139 +#: 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:140 +#: 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:141 +#: 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:145 +#: terrariumTranslations.py:150 msgid "Holds the name of the animal." msgstr "Holds the name of the animal." -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:151 msgid "Holds the type of the animal" msgstr "Holds the type of the animal" -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:152 msgid "Holds the gender of the animal." msgstr "Holds the gender of the animal." -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:153 msgid "Holds the day of birth of the animal." msgstr "Holds the day of birth of the animal." -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:154 msgid "Holds the species name of the animal." msgstr "Holds the species name of the animal." -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:155 msgid "Holds the latin name of the animal." msgstr "Holds the latin name of the animal." -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:156 msgid "Holds a small description about the animal." msgstr "Holds a small description about the animal." -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:157 msgid "Holds a link to more information." msgstr "Holds a link to more information." @@ -462,10 +510,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 +706,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: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 @@ -716,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:147 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" @@ -791,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" @@ -1536,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:161 +#: views/notifications.tpl:87 views/notifications.tpl:204 msgid "Title" msgstr "Title" @@ -1637,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" @@ -2145,62 +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 -msgid "User id" -msgstr "User id" +#: views/notifications.tpl:175 +msgid "Username" +msgstr "Username" + +#: views/notifications.tpl:179 +msgid "Proxy" +msgstr "Proxy" -#: views/notifications.tpl:147 +#: views/notifications.tpl:190 msgid "Messages" msgstr "Messages" -#: views/notifications.tpl:158 +#: views/notifications.tpl:201 msgid "Trigger" msgstr "Trigger" -#: views/notifications.tpl:164 +#: views/notifications.tpl:207 msgid "Message" msgstr "Message" -#: views/notifications.tpl:167 +#: views/notifications.tpl:210 msgid "Service" msgstr "Service" @@ -2824,38 +2907,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..53cc2e3f9 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-30 18:33+CEST\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -327,99 +327,147 @@ msgstr "" msgid "Holds the password for authentication with the mailserver if needed." msgstr "" +#: 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 "" + +#: terrariumTranslations.py:113 +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:119 +msgid "Holds your Twitter access token secret. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:121 +msgid "Holds the PushOver API token. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:122 +msgid "Holds the PushOver API user key. More information %shere%s" +msgstr "" + +#: terrariumTranslations.py:124 +msgid "Holds the Telegram Bot token. More information %shere%s" +msgstr "" + #: terrariumTranslations.py:125 -msgid "Choose your interface language." +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: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:130 +msgid "Choose your interface language." +msgstr "" + +#: terrariumTranslations.py:131 msgid "Holds the distance type used by distance sensors." msgstr "" -#: terrariumTranslations.py:127 terrariumTranslations.py:128 +#: terrariumTranslations.py:132 terrariumTranslations.py:133 msgid "Holds the username which can make changes (Administrator)." msgstr "" -#: terrariumTranslations.py:129 +#: terrariumTranslations.py:134 msgid "Enter the new password for the administration user. Leaving empty will not change the password!" msgstr "" -#: terrariumTranslations.py:130 +#: terrariumTranslations.py:135 msgid "Enter the current password in order to change the password." msgstr "" -#: terrariumTranslations.py:131 +#: terrariumTranslations.py:136 msgid "Toggle on or off full authentication. When on, you need to authenticate at all times." msgstr "" -#: terrariumTranslations.py:132 +#: terrariumTranslations.py:137 msgid "Holds the external calendar url." msgstr "" -#: terrariumTranslations.py:133 +#: terrariumTranslations.py:138 msgid "Toggle on or off horizontal graph legends. Reload the webinterface after changing the setting." msgstr "" -#: terrariumTranslations.py:134 +#: terrariumTranslations.py:139 msgid "Holds the soundcard that is used for playing audio." msgstr "" -#: terrariumTranslations.py:135 +#: terrariumTranslations.py:140 msgid "Holds the amount of power in Wattage that the Raspberry PI uses including all USB equipment." msgstr "" -#: terrariumTranslations.py:136 +#: terrariumTranslations.py:141 msgid "Holds the amount of euro/dollar per 1 kW/h (1 Kilowatt per hour)." msgstr "" -#: terrariumTranslations.py:137 +#: terrariumTranslations.py:142 msgid "Holds the amount of euro/dollar per 1000 liters water." msgstr "" -#: terrariumTranslations.py:138 +#: terrariumTranslations.py:143 msgid "Choose the temperature indicator. The software will recalculate to the chosen indicator." msgstr "" -#: terrariumTranslations.py:139 +#: 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:140 +#: terrariumTranslations.py:145 msgid "Holds the port number on which the software is listening for connections." msgstr "" -#: terrariumTranslations.py:141 +#: terrariumTranslations.py:146 msgid "Holds the port number on which the OWFS software is running. Leave empty to disable OWFS support." msgstr "" -#: terrariumTranslations.py:145 +#: terrariumTranslations.py:150 msgid "Holds the name of the animal." msgstr "" -#: terrariumTranslations.py:146 +#: terrariumTranslations.py:151 msgid "Holds the type of the animal" msgstr "" -#: terrariumTranslations.py:147 +#: terrariumTranslations.py:152 msgid "Holds the gender of the animal." msgstr "" -#: terrariumTranslations.py:148 +#: terrariumTranslations.py:153 msgid "Holds the day of birth of the animal." msgstr "" -#: terrariumTranslations.py:149 +#: terrariumTranslations.py:154 msgid "Holds the species name of the animal." msgstr "" -#: terrariumTranslations.py:150 +#: terrariumTranslations.py:155 msgid "Holds the latin name of the animal." msgstr "" -#: terrariumTranslations.py:151 +#: terrariumTranslations.py:156 msgid "Holds a small description about the animal." msgstr "" -#: terrariumTranslations.py:152 +#: terrariumTranslations.py:157 msgid "Holds a link to more information." msgstr "" @@ -461,10 +509,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 +705,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: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 @@ -715,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:147 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 "" @@ -790,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 "" @@ -1535,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:161 +#: views/notifications.tpl:87 views/notifications.tpl:204 msgid "Title" msgstr "" @@ -1636,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 "" @@ -2144,62 +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 -msgid "User id" +#: views/notifications.tpl:175 +msgid "Username" msgstr "" -#: views/notifications.tpl:147 +#: views/notifications.tpl:179 +msgid "Proxy" +msgstr "" + +#: views/notifications.tpl:190 msgid "Messages" msgstr "" -#: views/notifications.tpl:158 +#: views/notifications.tpl:201 msgid "Trigger" msgstr "" -#: views/notifications.tpl:164 +#: views/notifications.tpl:207 msgid "Message" msgstr "" -#: views/notifications.tpl:167 +#: views/notifications.tpl:210 msgid "Service" msgstr "" @@ -2823,38 +2906,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/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/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 7b96c14ef..68db3a84f 100644 --- a/static/js/terrariumpi.js +++ b/static/js/terrariumpi.js @@ -1272,28 +1272,30 @@ 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); } $('#' + 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'}); } }); } @@ -1512,7 +1514,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()); } @@ -2607,9 +2609,11 @@ $(document).ready(function() { $(this).on('click', load_page).attr('title',$(this).parents('li').find('a:first').text()); }); - $("
   
").css({ + $("
   
").css({ position: "absolute", - }).appendTo("body"); + }).appendTo("body").tooltip({html:true}).on('hidden.bs.tooltip',function(event){ + $('#tooltipGraph').hide(); + }) load_door_history(); load_player_status(); diff --git a/terrariumCollector.py b/terrariumCollector.py index 413b4700e..174378342 100644 --- a/terrariumCollector.py +++ b/terrariumCollector.py @@ -4,7 +4,6 @@ import sqlite3 import time -import json import copy import os @@ -27,8 +26,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 +37,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 +71,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 +89,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 +122,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 +135,51 @@ 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() + + 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') + 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() + logger.info('Collector database upgrade for version 3.8.0 succeeded! Removed duplicate records') def __recover(self): starttime = time.time() @@ -164,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) @@ -174,6 +218,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 +230,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 +249,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 + 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 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() + 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,19 +371,42 @@ 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 + 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 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 = ?' - filters = (stoptime,starttime,parameters[0],) + filters = (stoptime - (24 * 60 * 60),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 + 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 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 = ?' - filters = (stoptime,starttime,parameters[0],) + filters = (stoptime - (24 * 60 * 60),starttime,parameters[0],) elif logtype == 'weather': fields = { 'wind_speed' : [], 'temperature' : [], 'pressure' : [] , 'wind_direction' : [], 'rain' : [], @@ -418,55 +431,80 @@ 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)) - except sqlite3.DatabaseError as ex: - logger.error('TerrariumPI Collecter exception! %s', (ex,)) - if 'database disk image is malformed' == str(ex): - self.__recover() + first_item = 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 - for row in rows: - if logtype == 'switches' and len(row) == len(fields)+1: - for field in fields: - history[field] = row[field] + if row['type'] not in history: + history[row['type']] = {} - return history + 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 row['type'] not in history: - history[row['type']] = {} + 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]] = [] - 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 [] + 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 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]] = [] + else: + if row['id'] not in history[row['type']]: + history[row['type']][row['id']] = copy.deepcopy(fields) - 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 row['type'] in ['switches','doors']: + history[row['type']][row['id']]['totals'] = {'duration' : 0, 'power_wattage' : 0, 'water_flow' : 0} - else: - if row['id'] not in history[row['type']]: - history[row['type']][row['id']] = copy.deepcopy(fields) + 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']) - for field in fields: - history[row['type']][row['id']][field].append([row['timestamp'] * 1000,row[field]]) + 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'] - if logtype == 'switches': - self.__calculate_power_and_water_usage(history) - elif logtype == 'doors': - self.__calculate_door_usage(history) + for field in fields: + history[row['type']][row['id']][field].append([row['timestamp'] * 1000,row[field]]) + + 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]]) + + 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() + + # 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: + # 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]]) + + 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[logtype][parameters[0]][field].append([stoptime * 1000,0]) + history[logtype][parameters[0]][field].append([starttime * 1000,0]) return history 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 7cf7b9790..c36e81ea8 100644 --- a/terrariumEngine.py +++ b/terrariumEngine.py @@ -11,6 +11,7 @@ import thread import time +import datetime import uptime import os import psutil @@ -71,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') @@ -224,6 +225,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: @@ -370,27 +373,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['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 = self.collector.get_total_power_water_usage() totals['power_wattage']['wattage'] += totals['power_wattage']['duration'] * self.pi_power_wattage return totals @@ -445,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: @@ -455,6 +439,16 @@ def __engine_loop(self): except Exception, err: print err + 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: + 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) + 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)) @@ -921,7 +915,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'] 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: diff --git a/terrariumLCD.py b/terrariumLCD.py new file mode 100644 index 000000000..1943c9f61 --- /dev/null +++ b/terrariumLCD.py @@ -0,0 +1,289 @@ +# -*- 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 = ['Starting terrariumPI...'] + thread.start_new_thread(self.__rotate_messages, ()) + + def __rotate_messages(self): + max_lines = int(self.__resolution[1]) + + 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: + 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) + starttime = time.time() + + 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..e8d2a5b1a 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 @@ -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(), @@ -76,14 +73,15 @@ def get_data(self): 'services' : ','.join(self.services) } -class terrariumNotificationTelegramBot(threading.Thread): - __metaclass__ = Singleton +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 + self.__bot_token = bot_token self.__bot_url = 'https://api.telegram.org/bot{}/'.format(self.__bot_token) self.__chat_ids = [] @@ -94,37 +92,17 @@ 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 = 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) 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: @@ -144,7 +122,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 ''} def send_message(self,text, chat_id = None): if self.__running: @@ -152,7 +130,7 @@ 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) def set_valid_users(self,users = None): self.__valid_users = users.split(',') if users is not None else [] @@ -160,34 +138,42 @@ 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 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))) - def run(self): + 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']) - time.sleep(5) + sleep(5) - time.sleep(0.5) + sleep(0.5) except Exception, ex: + error_counter += 1 print ex - time.sleep(5) + 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): @@ -259,8 +245,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 +253,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 +303,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 +533,6 @@ def update_twitter_profile_image(self): except Exception, ex: print ex - def send_tweet(self,message): if self.twitter is None: return @@ -581,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: @@ -588,6 +580,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 +633,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 +663,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 +693,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/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", diff --git a/terrariumTranslations.py b/terrariumTranslations.py index 2768a1f06..a0d9b1c72 100644 --- a/terrariumTranslations.py +++ b/terrariumTranslations.py @@ -109,16 +109,21 @@ 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_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_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_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_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_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_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 +156,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/terrariumUtils.py b/terrariumUtils.py index 92fd59bed..188d31127 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,28 @@ 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} + response = requests.get(url,auth=(url_data['username'],url_data['password']),timeout=timeout,proxies=proxies) + + 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.... + try: + item = int(item) + except Exception, ex: + item = str(item) + + data = data[item] + else: + data = response.text - 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 [] - - 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: data = None @@ -235,3 +239,14 @@ 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)} + + @staticmethod + def format_uptime(value): + return str(datetime.timedelta(seconds=int(value))) + +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/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 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..fe40f3517 100644 --- a/views/notifications.tpl +++ b/views/notifications.tpl @@ -57,6 +57,45 @@ +
+
+
+
+

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

+ +
+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
@@ -171,7 +210,7 @@
-% for message in notifications.get_messages(): + % for message in notifications.get_messages():
{{message['id'].replace('_',' ').capitalize()}} @@ -198,7 +237,7 @@
-% end + % end
@@ -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; } }