-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwemoFetchClass.py
More file actions
410 lines (398 loc) · 22.3 KB
/
Copy pathwemoFetchClass.py
File metadata and controls
410 lines (398 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
import sqlite3
import datetime
import pymssql
import logging
import time
import socket
import sys
import uuid
import os
import atexit
from ouimeaux.utils import get_ip_address
from ouimeaux.environment import Environment
class LocalNetworkWemoFetcher:
def __init__(self,config_params):
# We will use an in-memory database & table to store and aggregate our data we've pulled from our WeMo devices
self.dbfile = str(uuid.uuid4()) + ".db"
self.db = sqlite3.connect(self.dbfile)
self.cur = self.db.cursor()
# This function will create the database to store our WeMo device data;
# since this is a simple example, it`s just one table:
self.cur.execute('''CREATE TABLE switchDataPoints (
MACAddress TEXT,
IPAddress TEXT,
SignalStrength INTEGER,
SerialNbr TEXT,
ModelNbr TEXT,
FirmwareVersion TEXT,
DeviceName TEXT,
Status INTEGER,
EnergyUse INTEGER,
DateDataFetched DATE
)'''
)
self.cur.execute('''CREATE TABLE averagedDataPoints (
MACAddress TEXT,
IPAddress TEXT,
SignalStrength INTEGER,
SerialNbr TEXT,
ModelNbr TEXT,
FirmwareVersion TEXT,
DeviceName TEXT,
Status INTEGER,
AvgEnergyUse INTEGER,
CountDataPoints INTEGER,
DateDataFetched DATE
)'''
)
self.config = config_params
self.wemoenvironment = None
# we will construct and destruct this elsewhere
def startStopWemoEnvironment(self, startstop):
if startstop == "start":
try:
try:
self.wemoenvironment = Environment()
except AttributeError:
self.wemoenvironment = Environment() #Create the variable if we destroyed it elsewhere
self.wemoenvironment.start()
self.wemoenvironment.discover(self.config.get("Seconds For Environment Discovery"))
except Exception as e:
logging.exception("Failed to initialize new wemo environment! " + str(e.message))
raise
if startstop == "stop":
try:
self.wemoenvironment.upnp.server.stop()
self.wemoenvironment.registry.server.stop()
del self.wemoenvironment
except Exception as e:
logging.exception("Failed to stop and delete wemo environment! " + str(e.message))
def getDeviceHardwareIDs(self, environment):
current_switches = self.wemoenvironment.list_switches()
if current_switches.__len__() == 0:
logging.exception("No devices exist in ouimeaux environment; cannot fetch hardware data")
raise NameError("No ouimeaux environment data exists!")
devicehardwaredata = []
for switchStr in (self.wemoenvironment.list_switches()):
currentswitch = self.wemoenvironment.get_switch(switchStr)
dict_switchinfo = currentswitch.basicevent.GetMacAddr()
switchmac = dict_switchinfo.get("MacAddr")
switchudnlowercase = dict_switchinfo.get("PluginUDN").lower()
dict_switchfirmwareversion = currentswitch.firmwareupdate.GetFirmwareVersion()
switchfirmwareversion = dict_switchfirmwareversion.get("FirmwareVersion")
switchipaddress = currentswitch.host
switchserialnumber = currentswitch.serialnumber
switchmodelnbr = currentswitch.model
dict_currentswitchattributes = {
"Device Name": switchStr,
"MAC Address": switchmac,
"Universal Unique Identifier": switchudnlowercase,
"Firmware Version": switchfirmwareversion,
"IP Address": switchipaddress,
"Serial Number": switchserialnumber,
"Model Number": switchmodelnbr
}
devicehardwaredata.append(
dict_currentswitchattributes
)
return devicehardwaredata
def closeconnection(self):
#Call this after ensuring data has been captured so that the temp db file is destroyed
self.db.close()
os.remove(self.dbfile)
def fetchdevicedata(self):
# print(Environment.list_switches()) #DEBUG: See what devices we grabbed during discovery
switchPowerDataArray = [] # We will store a list of power measurements in this list and then average them before sending them to a flat file or database (we don`t need 300 measurements per minute stored in the database; it should be flattened out)
# Fetch the current date/time into a variable, then find the date/time one minute from now; we'll use that
currentDateTime = datetime.datetime.now()
minuteFromNow = currentDateTime - datetime.timedelta(minutes=(-1 * self.config.get("Minutes to Gather Data")))
currentLoopIteration = 0 # We will only gather the switch hardware / firmware details at the first iteration of fetching power data; no need to get it multiple times during execution
deviceHardwareData = self.getDeviceHardwareIDs(self.wemoenvironment)
while datetime.datetime.now() <= minuteFromNow:
for wemoDevice in (deviceHardwareData):
currentSwitch = self.wemoenvironment.get_switch(wemoDevice.get("Device Name"))
print(currentSwitch)
switchsignalstrength = currentSwitch.basicevent.GetSignalStrength()
switchcurrentbinarystate = currentSwitch.basicevent.GetBinaryState()
switchhwinfo = currentSwitch.metainfo.GetMetaInfo()
switchmanufacture= currentSwitch.manufacture.GetManufactureData()
if currentSwitch.model.find('Insight') > 0:
if currentSwitch.insight_params.get("state") == 0:
#API sometimes show power usage when turned off; force usage to zero when off
switchpowerconsumption = 0
else: switchpowerconsumption = currentSwitch.current_power
switchcurrentstate = currentSwitch.insight_params.get("state")
datatoinsert = (
wemoDevice.get("MAC Address"),
wemoDevice.get("IP Address"),
float(switchsignalstrength.get("SignalStrength")),
wemoDevice.get("Serial Number"),
wemoDevice.get("Model Number"),
wemoDevice.get("Firmware Version"),
wemoDevice.get("Device Name"),
int(switchcurrentbinarystate.get("BinaryState")),
switchpowerconsumption
)
logging.info(datatoinsert)
self.cur.execute(
'''INSERT INTO switchDataPoints(
MACAddress
, IPAddress
, SignalStrength
, SerialNbr
, ModelNbr
, FirmwareVersion
, DeviceName
, Status
, EnergyUse
, DateDataFetched
) VALUES (?,?,?,?,?,?,?,?,?, datetime('now'))''',
datatoinsert) # This method must iterate through the list and replace the variables (?'s) in the INSERT statement from left to right
self.db.commit()
self.wemoenvironment.wait(self.config.get("Delay in Seconds When Fetching Data"))
derp = 2
def aggregateusagedata(self):
self.cur.execute(
'''INSERT INTO averagedDataPoints
SELECT
MACAddress
, MAX(IPAddress) AS IPAddress
, MIN(SignalStrength) AS SignalStrength
, SerialNbr
, MAX(ModelNbr) AS ModelNbr
, MAX(FirmwareVersion) AS FirmwareVersion
, DeviceName AS DeviceName
, MAX(Status) AS Status
, AVG(EnergyUse) AS EnergyUse
, COUNT(0) AS DataPointsCollected
, datetime('now') AS DataPulledDate
FROM switchDataPoints
GROUP BY MACAddress, SerialNbr, DeviceName
'''
)
self.db.commit()
self.cur.execute(
# Clear the ongoing log as we've already summarized and stored the averaged usage data
'''DELETE FROM switchDataPoints'''
)
tablequery = '''SELECT
ROWID
,MACAddress
,IPAddress
,SignalStrength
,SerialNbr
,ModelNbr
,FirmwareVersion
,DeviceName
,Status
,AvgEnergyUse
,CountDataPoints
,DateDataFetched
FROM averagedDataPoints'''
returnusagedata = []
for dataRow in self.cur.execute(tablequery):
rowdict = {
"SQLite3 - averagedDataPoints Row ID": dataRow[0]
,"MAC Address": dataRow[1]
,"IP Address": dataRow[2]
,"Signal Strength": dataRow[3]
, "Serial Number": dataRow[4]
, "Model Number": dataRow[5]
, "Firmware Version": dataRow[6]
, "Device Name": dataRow[7]
, "Device Status": dataRow[8]
, "Average Energy Usage": dataRow[9]
, "Data Points Collected": dataRow[10]
, "Date Stamp for Data": dataRow[11]
}
returnusagedata.append(rowdict)
return returnusagedata
def InsertOrUpdateDatabase(self,currentDataSet):
try:
#Connect to the MS SQL Server instance the database application is stored:
mssqldb = pymssql.connect(
self.config.get("server_ip")
, self.config.get("serviceaccount")
, self.config.get("db_password")
, self.config.get("databasename")
)
mssqlcursor = mssqldb.cursor()
for currentDataRow in currentDataSet:
print("Beginning work in MS SQL Server for ", currentDataRow.get("Device Name"))
#First, we need to fill the lookup tables before we can start filling the tables with FK's to the lookups:
mssqlcursor.execute("""
MERGE INTO dbo.deviceFirmware AS target
USING (SELECT
%s AS firmwareName
) AS source (firmwareName)
ON (target.firmwareName = source.firmwareName)
WHEN MATCHED THEN
UPDATE SET target.firmwareName = source.firmwareName
WHEN NOT MATCHED THEN
INSERT (firmwareName)
VALUES(source.firmwareName)
OUTPUT inserted.[deviceFirmwareSK] --This will return the new or fetched SK back to the calling client (Yay!!)
;
""",currentDataRow.get("Firmware Version")) #http://stackoverflow.com/questions/3410455/how-do-i-use-sql-parameters-with-python
currentFirmwareSK = mssqlcursor.fetchone()
mssqlcursor.execute("""
MERGE INTO dbo.networkMetadata AS target
USING (SELECT
%s AS ipAddress
,%s AS tcpIPversion
) AS source (ipAddress, tcpIPversion)
ON (target.ipAddress = source.ipAddress)
WHEN MATCHED THEN
UPDATE SET target.ipAddress = source.ipAddress, target.tcpIPversion = source.tcpIPversion
WHEN NOT MATCHED THEN
INSERT (ipAddress, tcpIPversion)
VALUES(source.ipAddress, source.tcpIPversion)
OUTPUT inserted.[networkMetadataSK]
;
""",(currentDataRow.get("IP Address"),'IPv4'))
currentNetworkMetadataSK = mssqlcursor.fetchone()
mssqlcursor.execute("""
MERGE INTO dbo.deviceTypes AS target
USING (SELECT
%s AS deviceTypeLabel
) AS source (deviceTypeLabel)
ON (target.deviceTypeLabel = source.deviceTypeLabel)
WHEN MATCHED THEN
UPDATE SET target.deviceTypeLabel= source.deviceTypeLabel
WHEN NOT MATCHED THEN
INSERT (deviceTypeLabel)
VALUES(source.deviceTypeLabel)
OUTPUT inserted.[deviceTypeSK]
;
""",(currentDataRow.get("Model Number")))
currentDeviceTypeSK = mssqlcursor.fetchone()
#Now that we have the SK's for our lookups, upsert into the IoTDevice table:
mssqlcursor.execute("""
MERGE INTO dbo.IoTDevice AS target
USING (SELECT
%s AS macAddress
,%s AS serialNumber
,%s AS friendlyName
,%s AS deviceTypeFK
,%s AS deviceFirmwareFK
,%s AS deviceIPAddressFK
,0 AS retiredDevice --If this merge statment is being called from the python app, then obviously the device is active
) AS source (
macAddress
,serialNumber
,friendlyName
,deviceTypeFK
,deviceFirmwareFK
,deviceIPAddressFK
,retiredDevice
)
ON (
target.macAddress = source.macAddress
AND target.serialNumber = source.serialNumber
)
--Honestly, this is bad code; you should likely return a SELECT to the application to see if an UPDATE is necessary. This will UPDATE a device record every time the application has data from a device. Lots and lots of unnecessary writes.
WHEN MATCHED THEN
UPDATE SET
target.macAddress = source.macAddress
, target.serialNumber = source.serialNumber
, target.friendlyName = source.friendlyName
, target.deviceTypeFK = source.deviceTypeFK
, target.deviceFirmwareFK = source.deviceFirmwareFK
, target.deviceIPAddressFK = source.deviceIPAddressFK
, target.retiredDevice = source.retiredDevice
, target.deviceChangedDate = getdate()
WHEN NOT MATCHED THEN
INSERT (
macAddress
,serialNumber
,friendlyName
,deviceTypeFK
,deviceFirmwareFK
,deviceIPAddressFK
,retiredDevice
,deviceChangedDate
)
VALUES(
source.macAddress
, source.serialNumber
, source.friendlyName
, source.deviceTypeFK
, source.deviceFirmwareFK
, source.deviceIPAddressFK
, source.retiredDevice
, getDate()
)
OUTPUT inserted.[deviceSK]
;
""",(currentDataRow.get("MAC Address"),currentDataRow.get("Serial Number"),currentDataRow.get("Device Name"),currentDeviceTypeSK,currentFirmwareSK,currentNetworkMetadataSK))
currentDeviceSK = mssqlcursor.fetchone()
mssqlcursor.execute("""
MERGE INTO dbo.powerScales AS target
USING (SELECT
%s AS unitOfPower
) AS source (unitOfPower)
ON (target.unitOfPower = source.unitOfPower)
WHEN MATCHED THEN
UPDATE SET target.unitOfPower = source.unitOfPower
,target.scaleChangedDate = getdate()
WHEN NOT MATCHED THEN
INSERT (unitOfPower, scaleAddedDate)
VALUES(source.unitOfPower, getdate())
OUTPUT inserted.powerScaleSK
;
""",('Milliwatt'))
currentPowerScaleSK = mssqlcursor.fetchone()
mssqlcursor.execute("""
MERGE INTO dbo.statusList AS target
USING (SELECT
%s AS statusNumberRepresentation
,%s AS sourceSystem
) AS source (statusNumberRepresentation, sourceSystem)
ON (
target.statusNumberRepresentation = source.statusNumberRepresentation
AND target.sourceSystem = source.sourceSystem
)
WHEN MATCHED THEN
UPDATE SET target.statusNumberRepresentation = source.statusNumberRepresentation
,target.sourceSystem = source.sourceSystem
,target.statusChangedDate = getdate()
WHEN NOT MATCHED THEN
INSERT (statusNumberRepresentation, sourceSystem, statusAddedDate)
VALUES(source.statusNumberRepresentation, source.sourceSystem, getdate())
OUTPUT inserted.statusSK
;
""",(currentDataRow.get("Device Status"),'OuimeauxPython'))
currentstatusSK = mssqlcursor.fetchone()
#Now that we've filled all the lookup tables for the device itself, we can store the usage data for that device (after ensuring that
mssqlcursor.execute("""
INSERT INTO dbo.deviceUsageData (
deviceFK
,deviceSignalStrength
,deviceStateFK
,devicePowerUsage
,devicePowerScaleFK
,dataPointSampleSize
,dataPointAddedDate
)
VALUES(
%s
,%s
,%s
,%s
,%s
,%s
,getdate()
)
;
""",(currentDeviceSK, currentDataRow.get("Signal Strength"), currentstatusSK, currentDataRow.get("Average Energy Usage"), currentPowerScaleSK, currentDataRow.get("Data Points Collected")))
mssqlcursor.execute("""
COMMIT;
"""
)
mssqldb.commit()
self.cur.execute("DELETE FROM averagedDataPoints WHERE ROWID = ?", (int(currentDataRow.get("SQLite3 - averagedDataPoints Row ID")),))
mssqldb.close() #end of for loop per device
except Exception as e:
logging.warning("ERROR IN LOADING DATABASE WITH CACHED DATA: " + str(e.message))
raise #Ideally, error handling should fill an in-memory python buffer that is flushed into the DB when the exception state clears, but this is a home project for data that has little value (unlike, say, money changing hands), so meh.
print("Finished with MS SQL Server work!")