-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
425 lines (358 loc) · 15.5 KB
/
Copy pathmain.py
File metadata and controls
425 lines (358 loc) · 15.5 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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
import os
import sys
if sys.platform == "linux":
os.environ["KIVY_AUDIO"] = "ffpyplayer"
os.environ["KIVY_VIDEO"] = "video_ffpyplayer"
# CONFIG has no Kivy dependency — safe to import before Window creation.
from util.configuration import CONFIG
# All Kivy Config.set calls MUST happen before any import that triggers
# Window creation (kivy.graphics, kivy.core.window, kivy.uix, etc.).
from kivy.config import Config
Config.set('kivy', 'keyboard_mode', 'systemandmulti')
Config.set('graphics', 'verify_gl_main_thread', '0')
Config.set('graphics', 'width', str(CONFIG.get_int('window', 'width', 800)))
Config.set('graphics', 'height', str(CONFIG.get_int('window', 'height', 480)))
# Now safe to import modules that create the Window / GL context
from networking.poller import POLLER
from services.homeassistant.homeassistant import HOME_ASSISTANT
from kivy.graphics import Line
from composites.TimerDrawer.timerdrawer import TIMER_DRAWER
from composites.Notifications.notificationcenter import NOTIFICATION_CENTER
from services.taskmanager.taskmanager import TASK_MANAGER
from interface.pihomescreenmanager import PIHOME_SCREEN_MANAGER
from components.Hamburger.hamburger import Hamburger
from util.phlog import PIHOME_LOGGER
from server.server import SERVER
from util.const import _TASK_SCREEN, GESTURE_CHECK, GESTURE_DATABASE, GESTURE_TRIANGLE, GESTURE_W, TEMP_DIR
from handlers.PiHomeErrorHandler import PiHomeErrorHandler
from networking.mqtt import MQTT
from services.wallpaper.wallpaper import WALLPAPER_SERVICE
import sys
import kivy
import platform
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
from components.Image.networkimage import NetworkImage
from kivy.graphics import Line
from components.Toast.toast import Toast
from components.Keyboard.keyboard import ensure_keyboard_attached
from kivy.core.window import Window
from util.helpers import get_app, simplegesture
from kivy.metrics import dp
from kivy.base import ExceptionManager
from kivy.clock import Clock
from kivy.core.image import Image as CoreImage
from kivy.gesture import Gesture
# Run PiHome on Kivy 2.0.0
kivy.require('2.0.0')
# Hide Cursor
Window.show_cursor = platform.system() == 'Darwin'
Window.keyboard_anim_args = {"d":.2,"t":"linear"}
Window.softinput_mode = 'below_target'
class PiHome(App):
app_menu_open = False
toast_open = False
web_conf = None
_last_bg_path = None
def __init__(self, **kwargs):
super(PiHome, self).__init__(**kwargs)
self.layout = FloatLayout()
self.height = CONFIG.get_int('window', 'height', 480)
self.width = CONFIG.get_int('window', 'width', 800)
self.toast = Toast(on_reset=self.remove_toast)
self.menu_button = Hamburger()
# Flag to indicate the application is running
self.is_running = True
def setup(self):
"""
Setup default windowing positions and initialize
application Screens
"""
Window.size = (self.width, self.height)
POLLER.register_api("https://cdn.pihome.io/conf.json", 60 * 2, self.update_conf)
Clock.schedule_interval(lambda _: self._run(), 1)
# Connect to home assistant
HOME_ASSISTANT.connect()
# Add a custom error handler for pihome
ExceptionManager.add_handler(PiHomeErrorHandler())
# the root widget
def build(self):
self.setup()
self.layout.size = (self.width, self.height)
self.layout.size_hint = (1,1)
self.layout.pos = (0,0)
self.layout.bind(on_touch_down=lambda _, touch:self.on_touch_down(touch))
self.layout.bind(on_touch_up=lambda _, touch:self.on_touch_up(touch))
self.layout.bind(on_touch_move=lambda _, touch:self.on_touch_move(touch))
self.menu_button.event_handler = lambda value: self.set_app_menu_open(value)
self.menu_button.size_hint = (None, None)
# Position hamburger at top-left; bind to Window size so it tracks
# the actual coordinate system regardless of density or resize behavior.
def _update_menu_pos(*_):
self.menu_button.pos = (dp(10), Window.height - self.menu_button.height - dp(5))
self.menu_button.bind(size=_update_menu_pos)
Window.bind(size=_update_menu_pos)
_update_menu_pos()
# Load default background into ScreenManager canvas
try:
from kivy.core.image import Image as CoreImage
default_bg = "./assets/images/default_background.jpg"
if os.path.exists(default_bg):
texture = CoreImage(default_bg, keep_data=True).texture
PIHOME_SCREEN_MANAGER.set_background_texture(texture, bool(WALLPAPER_SERVICE.allow_stretch))
except Exception as e:
PIHOME_LOGGER.error(f"Failed to load default background: {e}")
self.layout.add_widget(PIHOME_SCREEN_MANAGER)
self.layout.add_widget(TIMER_DRAWER)
# NOTIFICATION_CENTER is mounted INSIDE HomeScreen (see screens/Home/home.py),
# not at the app root — its ScrollView only renders correctly within a
# Screen's stencil-buffered Fbo on the Pi.
self.layout.add_widget(self.menu_button)
# Startup TaskManager
TASK_MANAGER.start(PIHOME_SCREEN_MANAGER.loaded_screens[_TASK_SCREEN])
return self.layout
def reload_configuration(self):
PIHOME_LOGGER.info("Configuration changes detected. Reloading services...")
# Re-read base.ini into memory — Kivy's SettingsPanel wrote to it externally
CONFIG.reload()
# Restart wallpaper service so it picks up new source/stretch settings
WALLPAPER_SERVICE.restart()
# Notify all screens so they can react to config changes
PIHOME_SCREEN_MANAGER.reload_all()
# App-root composites aren't under a screen, so reload_all()'s per-screen
# cascade can't reach them — refresh their theme explicitly.
for w in (TIMER_DRAWER, getattr(self, "menu_button", None)):
if w is None:
continue
try:
if hasattr(w, "on_config_update"):
w.on_config_update(CONFIG)
elif hasattr(w, "_apply_theme"):
w._apply_theme()
except Exception as e:
PIHOME_LOGGER.error(f"App-root theme refresh failed: {e}")
PIHOME_LOGGER.info("Configuration reload complete.")
def restart(self):
"""
Clean kivy widgets and restart the application
"""
self.root.clear_widgets()
self.stop()
return PiHome().run()
def get_size(self):
return (self.width, self.height)
# def goto_screen(self, screen, pin_required = True):
# """
# Navigate to a specific screen. If the PIN is required to access the
# screen, the pin pad will be displayed prompting the user to enter PIN
# """
# if self.manager.transition.direction == "down":
# self.manager.transition.direction = "up"
# else:
# self.manager.transition.direction = "down"
# pin_required = pin_required and self.screens[screen].requires_pin
# if pin_required:
# self.show_pinpad()
# self.pinpad.on_enter = lambda *args: self.goto_screen(screen, False)
# else:
# self.remove_pinpad()
# self.manager.current = screen
# if (screen == _SETTINGS_SCREEN):
# self.menu_button.opacity = 0
# else:
# self.menu_button.opacity = 1
def set_app_menu_open(self, open):
# if self.pinpad.opacity == 1:
# return
self.app_menu_open = open
if open == True:
# self.layout.add_widget(self.appmenu, index=1)
# self.appmenu.show_apps()
PIHOME_SCREEN_MANAGER.app_menu.show()
else:
# self.appmenu.reset()
# self.layout.remove_widget(self.appmenu)
PIHOME_SCREEN_MANAGER.app_menu.dismiss()
self.menu_button.is_open = False
def toggle_app_menu(self):
self.set_app_menu_open(not self.app_menu_open)
def on_touch_down(self, touch):
# start collecting points in touch.ud
# create a line to display the points
userdata = touch.ud
userdata['line'] = Line(points=(touch.x, touch.y))
return False
def on_touch_up(self, touch):
if 'line' not in touch.ud:
return
g = simplegesture('', list(zip(touch.ud['line'].points[::2], touch.ud['line'].points[1::2])))
# User Input Gesture
# print(self.gdb.gesture_to_str(g))
# print(GESTURE_DATABASE.gesture_to_str(g))
# print match scores between all known gestures
# print("check:", g.get_score(GESTURE_CHECK))
# use database to find the more alike gesture, if any
g2 = GESTURE_DATABASE.find(g, minscore=0.70)
# print(g2)
if g2:
if g2[1] == GESTURE_CHECK:
pass
# self.set_app_menu_open(not self.app_menu_open)
elif g2[1] == GESTURE_TRIANGLE:
# goto_screen(_DEVTOOLS_SCREEN)
pass
elif g2[1] == GESTURE_W:
# self.wallpaper_service.shuffle()
pass
def on_touch_move(self, touch):
# store points of the touch movement
try:
touch.ud['line'].points += [touch.x, touch.y]
return False
except (KeyError) as e:
pass
def get_screen_shot(self):
"""
Get a screenshot of the current screen
"""
Window.screenshot(name=TEMP_DIR + "/screenshot.png")
"""
Quit PiHome and clean up resources
"""
def quit(self):
self.is_running = False
PIHOME_LOGGER.info("Quit requested - initiating shutdown...")
# on_stop() will handle all cleanup
get_app().stop()
sys.exit("PiHome Terminated")
def remove_toast(self):
self.toast_open = False
self.layout.remove_widget(self.toast)
def show_toast(self, label, level = "info", timeout = 5):
if self.toast is None:
print("Failed to show toast: {}".format(label))
return False
if self.toast_open is True:
self.remove_toast()
self.toast_open = True
self.layout.add_widget(self.toast)
self.toast.pop(label=label, level=level, timeout=timeout)
return True
def update_conf(self, json):
# TODO validate json
# important
self.web_conf = json
def _run(self):
# Update background texture from wallpaper service
try:
wallpaper_path = WALLPAPER_SERVICE.current
allow_stretch = bool(WALLPAPER_SERVICE.allow_stretch)
if wallpaper_path and wallpaper_path != "":
# Skip if nothing changed — avoids redundant texture re-uploads
# and prevents Kivy canvas no-ops from the same cached Texture object
if wallpaper_path == self._last_bg_path:
return
from kivy.core.image import Image as CoreImage
from kivy.loader import Loader
if wallpaper_path.startswith('http'):
proxyimg = Loader.image(wallpaper_path)
if proxyimg.loaded:
PIHOME_SCREEN_MANAGER.set_background_texture(proxyimg.texture, allow_stretch)
self._last_bg_path = wallpaper_path
else:
captured_path = wallpaper_path
def on_img_load(instance):
PIHOME_SCREEN_MANAGER.set_background_texture(instance.texture, allow_stretch)
self._last_bg_path = captured_path
proxyimg.bind(on_load=on_img_load)
else:
if os.path.exists(wallpaper_path):
# nocache=True forces a fresh texture upload so that revisiting a
# previously-seen wallpaper doesn't silently return the old cached
# Texture object (which Kivy treats as a no-op on rect.texture =)
texture = CoreImage(wallpaper_path, keep_data=True, nocache=True).texture
PIHOME_SCREEN_MANAGER.set_background_texture(texture, allow_stretch)
self._last_bg_path = wallpaper_path
except Exception as e:
PIHOME_LOGGER.debug(f"Background update: {e}")
def _reload_background(self):
"""
Schedules a texture update on the Kivy main thread. Safe to call from
GPIO interrupt callbacks, background worker threads, or POLLER callbacks.
Any Loader/CoreImage work is intentionally deferred into _run() so it
always executes on the main thread.
"""
Clock.schedule_once(lambda _: self._run(), 0)
def on_start(self):
"""
When application has started, do the following:
- Setup MQTT Services
- If in debug mode, setup Profiler
"""
self._init_mqtt()
# Re-apply saved onboard LED state (sysfs does not persist across reboot)
from system.leds import set_leds
leds_on = CONFIG.get("devtools", "leds_enabled", "1").strip().lower() in ("1", "true")
set_leds(leds_on)
# Make temporary dir
if not os.path.exists(TEMP_DIR):
os.makedirs(TEMP_DIR)
# self.profile = cProfile.Profile()
# self.profile.enable()
SERVER.start_server()
# Detect and install any missing per-screen pip dependencies declared in
# screen manifests. Non-blocking (runs on its own daemon thread) and
# reports progress via the Notification Center.
from util.dependencies import ensure_screen_dependencies
ensure_screen_dependencies()
# Discover and start any per-screen background services declared in
# screen manifests (screens/<Name>/services/). Always-on at boot,
# independent of whether the user navigates to the owning screen.
from util.screen_services import load_screen_services
load_screen_services()
def _init_mqtt(self):
h = CONFIG.get('mqtt', 'host', "")
u = CONFIG.get('mqtt', 'user_id', "")
p = CONFIG.get('mqtt', 'password', "")
f = CONFIG.get('mqtt', 'feed', "pihome")
port = CONFIG.get_int('mqtt', 'port', 8883)
if u != "" and h != "" and p != "":
self.mqtt = MQTT(host=h, port=port, feed = f, user=u, password=p)
def on_stop(self):
PIHOME_LOGGER.info("=================================== PIHOME SHUTDOWN ===================================")
# Set shutdown flags and let daemon threads die automatically
try:
TIMER_DRAWER.shutdown()
except:
pass
try:
TASK_MANAGER.is_running = False
except:
pass
try:
HOME_ASSISTANT.is_shutting_down = True
except:
pass
try:
if hasattr(self, 'mqtt') and self.mqtt:
self.mqtt.client.loop_stop()
except:
pass
try:
SERVER.shutting_down = True
if SERVER.httpd:
SERVER.httpd.shutdown()
except:
pass
try:
from util.screen_services import shutdown_screen_services
shutdown_screen_services()
except:
pass
# self.profile.disable()
# self.profile.dump_stats('pihome.profile')
# self.profile.print_stats()
# Start PiHome
app = PiHome()
app.run()
# PiHome().run()