Skip to content

Commit 0b110f7

Browse files
committed
Merge branch 'release/0.3.4'
2 parents 58d1e88 + f7d1348 commit 0b110f7

19 files changed

+870
-587
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ sftp-config.json
3232
requirements.txt
3333
!.gitignore
3434
.idea/
35+
utils/downloads/

BrewPiProcess.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ def quit(self):
7272
else:
7373
print "Could not connect to socket of BrewPi process, maybe it just started and is not listening yet."
7474
print "Could not send quit message to BrewPi instance with pid %d!" % self.pid
75+
print "Killing it instead!"
76+
self.kill()
7577
return False
7678

7779
def kill(self):
@@ -115,7 +117,14 @@ def update(self):
115117
Returns: list of BrewPiProcess objects
116118
"""
117119
bpList = []
118-
matching = [p for p in psutil.process_iter() if any('python' in p.name() and 'brewpi.py'in s for s in p.cmdline())]
120+
matching = []
121+
122+
# some OS's (OS X) do not allow processes to read info from other processes.
123+
try:
124+
matching = [p for p in psutil.process_iter() if any('python' in p.name() and 'brewpi.py'in s for s in p.cmdline())]
125+
except psutil.AccessDenied:
126+
pass
127+
119128
for p in matching:
120129
bp = self.parseProcess(p)
121130
bpList.append(bp)

BrewPiUtil.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ def scriptPath():
9797
def removeDontRunFile(path='/var/www/do_not_run_brewpi'):
9898
if os.path.isfile(path):
9999
os.remove(path)
100-
print "BrewPi set to be automatically restarted by cron"
100+
if not sys.platform.startswith('win'): # cron not available
101+
print "BrewPi script will restart automatically."
101102
else:
102103
print "File do_not_run_brewpi does not exist at " + path
103104

@@ -107,17 +108,28 @@ def setupSerial(config):
107108
conn = None
108109
port = config['port']
109110
dumpSerial = config.get('dumpSerial', False)
111+
112+
error1 = None
113+
error2 = None
110114
# open serial port
111-
try:
112-
ser = serial.Serial(port, 57600, timeout=0.1) # use non blocking serial.
113-
except (OSError, serial.SerialException) as e:
114-
logMessage("Error opening serial port. Trying alternative serial port {0}.\n({1})".format(config['altport'], str(e)))
115+
tries = 0
116+
while ser is None and tries < 10:
115117
try:
116-
port = config['altport']
118+
port = config['port']
117119
ser = serial.Serial(port, 57600, timeout=0.1) # use non blocking serial.
118120
except (OSError, serial.SerialException) as e:
119-
logMessage("Error opening alternative serial port. Script will exit. Is your controller connected via USB?\n({0})".format(str(e)))
120-
exit(1)
121+
error1 = '{0}.\n({1})'.format(port, str(e))
122+
try:
123+
port = config['altport']
124+
ser = serial.Serial(port, 57600, timeout=0.1) # use non blocking serial.
125+
126+
except (OSError, serial.SerialException) as e:
127+
error2 = '{0}.\n({1})'.format(port, str(e))
128+
129+
tries += 1
130+
if not ser:
131+
time.sleep(1)
132+
121133

122134
# yes this is monkey patching, but I don't see how to replace the methods on a dynamically instantiated type any other way
123135
if dumpSerial:
@@ -134,4 +146,14 @@ def writeAndDump(data):
134146
sys.stderr.write(data)
135147
ser.read = readAndDump
136148
ser.write = writeAndDump
149+
150+
if not ser:
151+
logMessage("Error opening serial port {0}:".format(error1))
152+
logMessage("Error opening alternative serial port {0}:".format(error2))
153+
137154
return ser, conn
155+
156+
# remove extended ascii characters from string, because they can raise UnicodeDecodeError later
157+
def asciiToUnicode(s):
158+
s = s.replace(chr(0xB0),'&deg')
159+
return unicode(s, 'ascii', 'ignore')

LogMessages.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
A copy of this file is included with the python script, so it can parse it to extract the log strings.
2424
The python script receives the messages as a few IDs and values in a JSON string.
2525
It uses this file to expand that to the full message.
26-
Not storing the full strings, but only the ID on the Arduino saves a lot of PROGMEM space.
26+
Not storing the full strings, but only the ID on the controller saves a lot of PROGMEM space.
2727
At startup the python script extracts the version number from this file and compares it to its own local copy.
2828
It will give a warning when the two strings do not match.
2929
*/
@@ -57,7 +57,7 @@ enum errorMessages{
5757
enum warningMessages{
5858
// PiLink.cpp
5959
MSG(WARNING_COULD_NOT_PROCESS_SETTING, "Could not process setting"),
60-
MSG(WARNING_INVALID_COMMAND, "Invalid command received by Arduino: %c", character),
60+
MSG(WARNING_INVALID_COMMAND, "Invalid command received by controller: %c", character),
6161

6262
// OneWireTempSensor.cpp
6363
MSG(WARNING_TEMP_SENSOR_DISCONNECTED, "Temperature sensor disconnected pin %d, address %s", pinNr, addressString),

MigrateSettings.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Copyright 2013 BrewPi
2+
# This file is part of BrewPi.
3+
4+
# BrewPi is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
9+
# BrewPi is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
14+
# You should have received a copy of the GNU General Public License
15+
# along with BrewPi. If not, see <http://www.gnu.org/licenses/>.
16+
17+
from collections import namedtuple, OrderedDict
18+
from distutils.version import LooseVersion
19+
import unittest
20+
21+
# SetttingMigrate containes 3 values:
22+
# key: the JSON key for the version in maxVersion
23+
# minVersion: the minimum version to restore from. Use 0 when all are valid.
24+
# maxVersion: the maximum version to restore to. Use 1000 when the most current release is still valid
25+
# alias: alternative keys from previous versions that should be interpreted as new key
26+
#
27+
SettingMigrate = namedtuple('SettingMigrate', ['key', 'minVersion', 'maxVersion', 'aliases'])
28+
29+
MigrateSettingsDefaultRestoreValidity = [
30+
SettingMigrate('tempFormat', '0', '1000', []),
31+
SettingMigrate('tempSetMin', '0', '1000', []),
32+
SettingMigrate('tempSetMax', '0', '1000', []),
33+
SettingMigrate('Kp', '0', '1000', []),
34+
SettingMigrate('Ki', '0', '1000', []),
35+
SettingMigrate('Kd', '0', '1000', []),
36+
SettingMigrate('iMaxErr', '0', '1000', []),
37+
SettingMigrate('pidMax', '0.2.4', '1000', []),
38+
SettingMigrate('idleRangeH', '0', '1000', []),
39+
SettingMigrate('idleRangeL', '0', '1000', []),
40+
SettingMigrate('heatTargetH', '0', '1000', []),
41+
SettingMigrate('heatTargetL', '0', '1000', []),
42+
SettingMigrate('coolTargetH', '0', '1000', []),
43+
SettingMigrate('coolTargetL', '0', '1000', []),
44+
SettingMigrate('maxHeatTimeForEst', '0', '1000', []),
45+
SettingMigrate('maxCoolTimeForEst', '0', '1000', []),
46+
SettingMigrate('fridgeFastFilt', '0.2.0', '1000', []),
47+
SettingMigrate('fridgeSlowFilt', '0.2.0', '1000', []),
48+
SettingMigrate('fridgeSlopeFilt', '0.2.0', '1000', []),
49+
SettingMigrate('beerFastFilt', '0.2.0', '1000', []),
50+
SettingMigrate('beerSlowFilt', '0.2.3', '1000', []),
51+
SettingMigrate('beerSlopeFilt', '0.2.3', '1000', []),
52+
SettingMigrate('lah', '0', '1000', []),
53+
SettingMigrate('hs', '0', '1000', []),
54+
SettingMigrate('heatEst', '0', '1000', []),
55+
SettingMigrate('coolEst', '0', '1000', []),
56+
SettingMigrate('fridgeSet', '0', '1000', []),
57+
SettingMigrate('beerSet', '0', '1000', []),
58+
SettingMigrate('mode', '0', '1000', [])
59+
]
60+
61+
class MigrateSettings:
62+
63+
def __init__(self, rv = None):
64+
'''
65+
:param rv: list of SettingMigrate namedtuples in the order they need to be restored
66+
'''
67+
if(rv == None):
68+
self.restoreValidity = MigrateSettingsDefaultRestoreValidity
69+
else:
70+
self.restoreValidity = rv
71+
72+
def getKeyValuePairs(self, oldSettings, oldVersion, newVersion):
73+
'''
74+
Settings are in order to restore them and are read from the old settings
75+
Versions are compared to see which settings are still considered valid
76+
77+
Keyword arguments:
78+
:param oldSettings: a dict of settings
79+
:param oldVersion: a string with the old version number
80+
:param newVersion: a string with the new version number
81+
:return keyValuePairs: OrderedDict of settings to restore
82+
:return oldSettings: settings that are not restored
83+
'''
84+
keyValuePairs = OrderedDict()
85+
oldSettingsCopy = oldSettings.copy() # get copy because we are removing items from the dict
86+
for setting in self.restoreValidity:
87+
for oldKey in [setting.key] + setting.aliases:
88+
if oldKey in oldSettingsCopy:
89+
if (LooseVersion(oldVersion) >= LooseVersion(setting.minVersion) and
90+
LooseVersion(newVersion) <= LooseVersion(setting.maxVersion)):
91+
keyValuePairs[setting.key] = oldSettingsCopy.pop(oldKey)
92+
break
93+
return keyValuePairs, oldSettingsCopy
94+
95+
96+
97+
class TestSettingsMigrate(unittest.TestCase):
98+
99+
def testMinVersion(self):
100+
''' Test if key is omitted when oldVersion < minVersion'''
101+
mg = MigrateSettings([
102+
SettingMigrate('key1', '0.2.0', '1000', []),
103+
SettingMigrate('key2', '0.1.1', '1000', []),
104+
])
105+
oldSettings = {'key1': 1, 'key2': 2}
106+
restored, omitted = mg.getKeyValuePairs(oldSettings, '0.1.8', '0.3.0')
107+
self.assertEqual(restored,
108+
OrderedDict([('key2', 2)]),
109+
"Should only return key2")
110+
111+
112+
def testMaxVersion(self):
113+
''' Test if key is omitted when newVersion > maxVersion'''
114+
mg = MigrateSettings([
115+
SettingMigrate('key1', '0.2.0', '0.3.0', []),
116+
SettingMigrate('key2', '0.1.1', '1000', []),
117+
])
118+
oldSettings = {'key1': 1, 'key2': 2}
119+
restored, omitted = mg.getKeyValuePairs(oldSettings, '0.3.0', '0.4.0')
120+
self.assertEqual(restored,
121+
OrderedDict([('key2', 2)]),
122+
"Should only return key2")
123+
124+
def testReturningNotRestored(self):
125+
mg = MigrateSettings([
126+
SettingMigrate('key1', '0.2.0', '0.3.0', []),
127+
SettingMigrate('key2', '0.1.1', '1000', []),
128+
])
129+
oldSettings = {'key1': 1, 'key2': 2}
130+
restored, omitted = mg.getKeyValuePairs(oldSettings, '0.3.0', '0.4.0')
131+
self.assertEqual(restored,
132+
OrderedDict([('key2', 2)]),
133+
"Should only return key2")
134+
135+
136+
137+
def testAliases(self):
138+
''' Test if aliases for old keys result in the new key being returned with the old value'''
139+
mg = MigrateSettings([ SettingMigrate('key1', '0', '1000', ['key1a', 'key1b'])])
140+
oldSettings = {'key1a': 1}
141+
restored, omitted = mg.getKeyValuePairs(oldSettings, '1', '1')
142+
self.assertEqual(restored, OrderedDict([('key1', 1)]))
143+
144+
145+
def testBrewPiFilters(self):
146+
''' Test if filters are only restored when old version > 0.2. The filter format was different earlier'''
147+
mg = MigrateSettings()
148+
oldSettings = {'fridgeFastFilt': 4}
149+
for oldVersion in ['0.1.0', '0.1.9', '0.1', '0.1.9.1']:
150+
restored, omitted = mg.getKeyValuePairs(oldSettings, oldVersion, '0.2.8')
151+
self.assertFalse('fridgeFastFilt' in restored,
152+
"Filter settings should be omitted when older than version 0.2.0" +
153+
", failed on version " + oldVersion)
154+
for oldVersion in ['0.2.0', '0.2.4', '0.3', '1.0']:
155+
restored, omitted = mg.getKeyValuePairs(oldSettings, oldVersion, '2.0')
156+
self.assertTrue('fridgeFastFilt' in restored,
157+
"Filter settings should be used when restoring from newer than version 0.2.0" +
158+
", failed on version " + oldVersion)
159+
160+
161+
def testPidMax(self):
162+
''' Test if filters are only restored when old version > 0.2.4 It was not outputed correctly earlier'''
163+
mg = MigrateSettings()
164+
oldSettings = {'pidMax': 10.0}
165+
for oldVersion in ['0.1.0', '0.2', '0.2.3']:
166+
restored, omitted = mg.getKeyValuePairs(oldSettings, oldVersion, '0.2.8')
167+
self.assertFalse('pidMax' in restored,
168+
"pidMax can only be trusted from version 0.2.4 or higher" +
169+
", failed on version " + oldVersion)
170+
for oldVersion in ['0.2.4', '0.2.5', '0.3', '1.0']:
171+
restored, omitted = mg.getKeyValuePairs(oldSettings, oldVersion, '2.0')
172+
self.assertTrue('pidMax' in restored,
173+
"pidMax should be restored when restoring form version " + oldVersion)
174+
175+
176+
def testAllBrewPiSettings(self):
177+
''' Test that when restoring from version 0.2.7 to 0.2.7 all settings are migrated'''
178+
from random import randint
179+
180+
mg = MigrateSettings()
181+
oldSettings = dict()
182+
for setting in mg.restoreValidity:
183+
oldSettings[setting.key] = randint(0,100) # use random integer for old settings
184+
restored, omitted = mg.getKeyValuePairs(oldSettings, '0.2.7', '0.2.7')
185+
186+
self.assertEqual(len(restored), len(oldSettings), "old and new settings should have same nr or items")
187+
188+
count = 0
189+
for setting in restored:
190+
if count == 0:
191+
self.assertEqual(setting, 'tempFormat', "tempFormat should be restored as first setting")
192+
self.assertEqual(restored[setting], oldSettings[setting], "old value and restored value do not match")
193+
count += 1
194+
195+
196+
if __name__ == "__main__":
197+
unittest.main()
198+

0 commit comments

Comments
 (0)