Skip to content

Commit b6d2a42

Browse files
Initial Commit
1 parent 5a4b4a7 commit b6d2a42

File tree

6 files changed

+199
-3
lines changed

6 files changed

+199
-3
lines changed

Modules/RoomControl/MagicHueAPI.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import typing
23

34
import magichue
45
import asyncio
@@ -126,6 +127,7 @@ def __init__(self, api, macaddr, database=None):
126127
super().__init__(macaddr, database=database)
127128
self.online = False
128129
self.api = api
130+
self.light = None # type: magichue.RemoteLight or None
129131

130132
if database is not None:
131133
cursor = database.cursor()
@@ -294,6 +296,21 @@ def is_on(self):
294296
else:
295297
return False
296298

299+
@background
300+
def set_custom_mode(self, speed: int, colors: list):
301+
if self.online:
302+
try:
303+
mode = magichue.CustomMode("CustomMode", speed, colors)
304+
self.light.mode = mode
305+
except magichue.exceptions.MagicHueAPIError as e:
306+
print(f"{self.macaddr} set custom mode error: {e}")
307+
else:
308+
print(f"{self.macaddr} is offline")
309+
310+
@background
311+
def set_mode(self, mode):
312+
pass
313+
297314
def get_status(self):
298315
if self.online:
299316
try:
@@ -304,7 +321,7 @@ def get_status(self):
304321
"white": self.light.w,
305322
"cold_white": self.light.cw,
306323
"white_enabled": self.light.is_white,
307-
"mode": self.light.mode._status_text(),
324+
"mode": self.light.mode.name(),
308325
"control_type": "MANUAL" if not self.is_auto else "AUTOMATIC",
309326
}
310327
return status
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import datetime
2+
import sqlite3
3+
import time
4+
5+
from Modules.RoomControl.AbstractSmartDevices import background
6+
import logging
7+
import pythonping
8+
9+
logging = logging.getLogger(__name__)
10+
11+
12+
class Device:
13+
14+
def __init__(self, name: str, database: sqlite3.Connection):
15+
self.name = name
16+
self.database = database
17+
self.entry = self.database.run("SELECT * FROM network_occupancy WHERE name=?", (name,))
18+
self.entry = self.entry.fetchone()
19+
20+
# Database values
21+
self.on_campus = self.entry[1]
22+
self.last_seen = datetime.datetime.fromtimestamp(self.entry[2])
23+
self.ip_address = self.entry[3]
24+
self.last_ip_update = datetime.datetime.fromtimestamp(self.entry[4])
25+
26+
# Other values
27+
self.bad_ip = False
28+
self.missed_pings = 0
29+
30+
def _validate_ip(self):
31+
# The subnet is 141.219.x.x
32+
return self.ip_address.startswith("141.219.")
33+
34+
def get_name(self):
35+
return self.name
36+
37+
def get_ip(self):
38+
return self.ip_address
39+
40+
def get_last_ip_update(self):
41+
return self.last_ip_update
42+
43+
def get_last_seen(self):
44+
return self.last_seen
45+
46+
def on_campus(self):
47+
return self.on_campus
48+
49+
def ping(self):
50+
# Pings the device and returns True if the ping was successful
51+
try:
52+
logging.info(f"Pinging {self.name}")
53+
timeout = 1
54+
response = pythonping.ping(self.ip_address, count=1, timeout=timeout)
55+
if response.rtt_avg_ms < timeout * 1000:
56+
logging.info(f"Ping successful for {self.name}, RTT: {response.rtt_avg_ms} ms")
57+
self.missed_pings = 0
58+
self.last_seen = datetime.datetime.now()
59+
self.on_campus = True
60+
return True
61+
else:
62+
self.missed_pings += 1
63+
logging.info(f"Ping unsuccessful for {self.name}, missed {self.missed_pings} pings")
64+
if self.missed_pings > 4:
65+
self.on_campus = False
66+
self.missed_pings = 0
67+
return False
68+
except Exception as e:
69+
logging.error(f"Error pinging {self.name}: {e}")
70+
return False
71+
72+
def needs_ping(self):
73+
# Returns True if the device needs to be pinged
74+
if self.on_campus:
75+
if datetime.datetime.now() - self.last_seen > datetime.timedelta(minutes=2):
76+
return True
77+
elif self.missed_pings > 0:
78+
return True
79+
else:
80+
return False
81+
else:
82+
if datetime.datetime.now() - self.last_seen > datetime.timedelta(minutes=1):
83+
return True
84+
else:
85+
return False
86+
87+
def update_db(self):
88+
# Updates the database with the current values
89+
self.database.run("UPDATE network_occupancy SET on_campus=?, last_seen=? WHERE name=?",
90+
(self.on_campus, self.last_seen.timestamp(), self.name))
91+
92+
def fetch_ip(self):
93+
# The device ip is updated by the device in the database, so periodically fetch the ip from the database
94+
if datetime.datetime.now() - self.last_ip_update > datetime.timedelta(minutes=5):
95+
values = self.database.run("SELECT ip_address, last_ip_update FROM network_occupancy WHERE name=?", (self.name,))
96+
self.ip_address = values[0]
97+
self.last_ip_update = datetime.datetime.fromtimestamp(values[1])
98+
99+
100+
class NetworkOccupancyDetector:
101+
"""
102+
Pings devices to see if they are on campus, and updates the database accordingly
103+
Devices that are believed to be on campus are pinged 2 minutes
104+
If a device misses a ping then it is pinged 4 times in 15 second intervals and if it misses
105+
all 4 then it is assumed to be off campus and the database is updated
106+
Devices that are believed to be off campus are pinged every minute
107+
"""
108+
109+
def __init__(self, database: sqlite3.Connection):
110+
self.database = database
111+
self.init_database()
112+
self.devices = []
113+
self.load_devices()
114+
self.periodic_refresh()
115+
116+
def init_database(self):
117+
self.database.run(
118+
"CREATE TABLE IF NOT EXISTS network_occupancy (name TEXT, on_campus BOOLEAN, last_seen INTEGER, "
119+
"ip_address TEXT, last_ip_update INTEGER)")
120+
121+
def load_devices(self):
122+
cursor = self.database.cursor()
123+
cursor.execute("SELECT * FROM network_occupancy")
124+
rows = cursor.fetchall()
125+
for row in rows:
126+
self.devices.append(Device(row[0], self.database))
127+
128+
def valid_ip(self, ip: str):
129+
# Checks if the IP address is a valid MTU IP address (subnet 141.219.x.x)
130+
return ip.startswith("141.219.")
131+
132+
def is_on_campus(self, name: str):
133+
# Returns True if the device is on campus
134+
for device in self.devices:
135+
if device.get_name() == name:
136+
return device.on_campus
137+
return False
138+
139+
@background
140+
def periodic_refresh(self):
141+
logging.info("Starting periodic refresh")
142+
while True:
143+
try:
144+
for device in self.devices:
145+
if device.needs_ping():
146+
device.ping()
147+
device.update_db()
148+
except Exception as e:
149+
logging.error(f"Error in periodic refresh: {e}")
150+
finally:
151+
time.sleep(15) # Sleep for 15 seconds

Modules/RoomControl/OccupancyDetection/OccupancyDetector.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44

55
from Modules.RoomControl.AbstractSmartDevices import background
6+
from Modules.RoomControl.OccupancyDetection.MTUNetOccupancy import NetworkOccupancyDetector
67

78
logging = logging.getLogger(__name__)
89

@@ -23,6 +24,8 @@ def __init__(self, database):
2324
self.last_activity = 0 # type: int # Last time a user was detected either by door or motion sensor
2425

2526
self.blue_stalker = BluetoothDetector(self.database, high_frequency_scan_enabled=False if GPIO else True)
27+
self.net_stalker = NetworkOccupancyDetector(self.database)
28+
2629
if GPIO:
2730
GPIO.setmode(GPIO.BOARD)
2831

@@ -96,6 +99,9 @@ def was_activity_recent(self, seconds=60):
9699
def is_here(self, device):
97100
return self.blue_stalker.is_here(device)
98101

102+
def on_campus(self, device):
103+
return self.net_stalker.is_on_campus(device)
104+
99105
def get_name(self, device):
100106
return self.blue_stalker.get_name(device)
101107

ip_update.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import datetime
2+
import sqlite3
3+
import argparse
4+
5+
# This python program is called remotely over SSH by a phone to allow it to update its ip address in the database
6+
7+
parser = argparse.ArgumentParser(description="Update the ip address of a device in the database")
8+
parser.add_argument("device", help="The name of the device to update")
9+
parser.add_argument("ip", help="The ip address of the device")
10+
args = parser.parse_args()
11+
12+
database = sqlite3.connect("room_data.db", check_same_thread=False)
13+
cursor = database.cursor()
14+
15+
cursor.execute("UPDATE devices SET ip_address = ?, last_ip_update = ? WHERE name = ?",
16+
(args.ip, datetime.datetime.now().timestamp(), args.device))
17+
18+
database.commit()
19+
database.close()

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import logging
2+
23
# %(name)s.%(funcName)s
34
logging.basicConfig(level=logging.INFO,
45
format=r"%(levelname)s - %(threadName)s - %(message)s",
56
datefmt='%H:%M:%S')
67

7-
88
from Modules import RoomControl
99
import asyncio
1010
from Modules.RoomControl.AbstractSmartDevices import background

requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ urllib3==1.26.12
99
wheel==0.37.1
1010
aiohttp==3.8.1
1111
pyvesync~=2.0.4
12-
pybluez==0.23
12+
pybluez==0.23
13+
multidict~=6.0.2
14+
psutil~=5.9.2
15+
pythonping~=1.1.4

0 commit comments

Comments
 (0)