def set_resolution(self, resolution: Tuple[int, int]) -> None: """Sets the current output to the given resolution. Also updates the background in production.""" if not self.initialized: return subprocess.call([ "xrandr", "--output", self.output, "--mode", util.format_resolution(resolution), ]) if not conf.DEBUG: # show a background that is visible when the visualization is not rendering # needs to be reset every resolution change try: subprocess.call([ "feh", "--bg-max", os.path.join(conf.BASE_DIR, "resources/images/background.png"), ]) except FileNotFoundError: pass redis.put("current_resolution", resolution) self.resolution = resolution lights.update_state()
def __init__(self, manager) -> None: super().__init__(manager, "wled") self.led_count = storage.get("wled_led_count") self.ip = storage.get("wled_ip") if not self.ip: try: device = util.get_devices()[0] broadcast = util.broadcast_of_device(device) self.ip = broadcast except Exception: # pylint: disable=broad-except # we don't want the startup to fail # just because the broadcast address could not be determined self.ip = "127.0.0.1" storage.put("wled_ip", self.ip) self.port = storage.get("wled_port") self.header = bytes([ 2, # DRGB protocol, we update every led every frame 1, # wait 1 second after the last packet until resuming normally ]) self.initialized = True redis.put("wled_initialized", True)
def _scan_bluetooth() -> None: bluetoothctl = _start_bluetoothctl() redis.put("bluetooth_devices", []) assert bluetoothctl and bluetoothctl.stdin bluetoothctl.stdin.write(b"devices\n") bluetoothctl.stdin.write(b"scan on\n") bluetoothctl.stdin.flush() while True: line = _get_bluetoothctl_line(bluetoothctl) if not line: break # match old devices match = re.match(r"Device (\S*) (.*)", line) # match newly scanned devices # We need the '.*' at the beginning of the line to account for control sequences if not match: match = re.match(r".*\[NEW\] Device (\S*) (.*)", line) if match: address = match.group(1) name = match.group(2) # filter unnamed devices # devices named after their address are no speakers if re.match("[A-Z0-9][A-Z0-9](-[A-Z0-9][A-Z0-9]){5}", name): continue bluetooth_devices = redis.get("bluetooth_devices") bluetooth_devices.append({"address": address, "name": name}) redis.put("bluetooth_devices", bluetooth_devices) settings.update_state()
def trigger_alarm(_request: WSGIRequest) -> None: """Manually triggers an alarm.""" playback.trigger_alarm() # because a state update is sent after every control (including this one) # a state update with alarm not being set would be sent # prevent this by manually setting this redis variable prematurely redis.put("alarm_playing", True)
def _start_bluetoothctl() -> Optional[subprocess.Popen]: if redis.get("bluetoothctl_active"): return None redis.put("bluetoothctl_active", True) bluetoothctl = subprocess.Popen(["bluetoothctl"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) return bluetoothctl
def update_user_count() -> None: """Go through all recent requests and delete those that were too long ago.""" now = time.time() last_requests = redis.get("last_requests") for key, value in list(last_requests.items()): if now - value >= INACTIVITY_PERIOD: del last_requests[key] redis.put("last_requests", last_requests) redis.put("last_user_count_update", now)
def __init__(self) -> None: self.loop_active: Optional[Event] = Event() self.devices = Devices(Ring(self), Strip(self), WLED(self), Screen(self)) # these settings are mirrored from the database, # because some of them are accessed multiple times per update. self.settings: Settings = { "ups": storage.get("ups"), "dynamic_resolution": storage.get("dynamic_resolution"), "program_speed": storage.get("program_speed"), "fixed_color": storage.get("fixed_color"), "last_fixed_color": storage.get("fixed_color"), } self.utilities = Utilities(Disabled(self), Cava(self), Alarm(self)) cava_installed = shutil.which("cava") is not None # a dictionary containing all led programs by their name led_programs: Dict[str, LedProgram] = { self.utilities.disabled.name: self.utilities.disabled } led_program_classes = [Fixed, Rainbow] if cava_installed: led_program_classes.append(Adaptive) for led_program_class in led_program_classes: led_program = led_program_class(self) led_programs[led_program.name] = led_program # a dictionary containing all screen programs by their name screen_programs: Dict[str, ScreenProgram] = { self.utilities.disabled.name: self.utilities.disabled } logo_loop = Video(self, "LogoLoop.mp4", loop=True) screen_programs[logo_loop.name] = logo_loop if cava_installed: for variant in sorted(Visualization.get_variants()): screen_programs[variant] = Visualization(self, variant) redis.put("led_programs", list(led_programs.keys())) redis.put("screen_programs", list(screen_programs.keys())) # a dictionary containing *all* programs by their name self.programs: Dict[str, LightProgram] = { **led_programs, **screen_programs } for device in self.devices: device.load_program() self.consumers_changed() self.listener = Thread(target=self.listen_for_changes) self.listener.start()
def consumers_changed(self) -> None: """Stops the loop if no led is active, starts it otherwise""" if self.utilities.disabled.consumers == 4: assert self.loop_active self.loop_active.clear() redis.put("lights_active", False) else: assert self.loop_active self.loop_active.set() redis.put("lights_active", True)
def stop_workers() -> None: # wake up the playback thread and stop it redis.put("stop_playback_loop", True) playback.queue_changed.set() # wake the buzzer thread so it exits playback.buzzer_stopped.set() # wake up the listener thread with an instruction to stop the lights worker redis.connection.publish("lights_settings_changed", "stop")
def __init__(self, manager, name) -> None: self.manager = manager self.name = name assert self.name in ["ring", "strip", "wled", "screen"] self.brightness = storage.get( cast(DeviceBrightness, f"{self.name}_brightness")) self.monochrome = storage.get( cast(DeviceMonochrome, f"{self.name}_monochrome")) self.initialized = False redis.put(cast(DeviceInitialized, f"{self.name}_initialized"), False) self.program: LightProgram = Disabled(manager)
def adjust(self) -> None: """Updates resolutions and resets the current one. Needed after changing screens or hotplugging after booting without a connected screen.""" self.resolution = storage.get("initial_resolution") resolutions = list(reversed(sorted(self.list_resolutions()))) redis.put("resolutions", resolutions) # if unset, initialize with the highest resolution if self.resolution == (0, 0): storage.put("initial_resolution", resolutions[0]) self.resolution = resolutions[0] self.set_resolution(self.resolution)
def set_bluetooth_scanning(request: WSGIRequest) -> HttpResponse: """Enables scanning of bluetooth devices.""" enabled = request.POST.get("value") == "true" if enabled: if redis.get("bluetoothctl_active"): return HttpResponseBadRequest("Already Scanning") _scan_bluetooth.delay() return HttpResponse("Started scanning") if not redis.get("bluetoothctl_active"): return HttpResponseBadRequest("Currently not scanning") # this is another request, so we don't have a handle of the current bluetoothctl process # terminate the process by name and release the lock subprocess.call("pkill bluetoothctl".split()) redis.put("bluetoothctl_active", False) return HttpResponse("Stopped scanning")
def compute(self) -> None: now = time.time() if now - self.last_fps_check > Visualization.FPS_MEASURE_WINDOW / 2: self.last_fps_check = now current_fps = self.controller.get_fps() redis.put("current_fps", current_fps) if ( self.manager.settings["dynamic_resolution"] and current_fps < 0.9 * self.manager.settings["ups"] ): self.manager.devices.screen.lower_resolution() # restart the program with the new resolution self.manager.restart_screen_program(sleep_time=2, has_lock=True) else: lights.update_state() if not self.controller.is_active(): raise ScreenProgramStopped self.controller.set_parameters( self.manager.utilities.alarm.factor, self.manager.utilities.cava.current_frame, )
def __init__(self, manager) -> None: super().__init__(manager, "screen") # set the DISPLAY environment variable the correct X Display is used os.environ["DISPLAY"] = ":0" # the visualization needs X to work, so we check if it is running try: subprocess.check_call("xset q".split(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except (FileNotFoundError, subprocess.CalledProcessError): # Cannot connect to X return # disable blanking and power saving subprocess.call("xset s off".split()) subprocess.call("xset s noblank".split()) subprocess.call("xset -dpms".split()) # ignore the scale factor for large displays, # we always render fullscreen without scaling os.environ["WINIT_X11_SCALE_FACTOR"] = "1" # method should additionally check whether hdmi is connected # however, I found no way to do that # without hdmi_force_hotplug=1: # tvservice -M gives attached events, but tvserice -s is always connected # hdmi cannot be plugged in after boot # with hdmi_force_hotplug=1: # tvservice -M records nothing, tvserice -s is always connected # /sys/class/drm/card1-HDMI-A-1/status is always connected # # so we set hotplug and initialize the screen even if none is connected self.initialized = True redis.put("screen_initialized", True) self.output = self.get_primary() self.resolution = (0, 0) # set in adjust self.adjust()
def __init__(self, manager) -> None: super().__init__(manager, "strip") self.monochrome = True try: # https://github.com/adafruit/Adafruit_CircuitPython_PCA9685/blob/main/examples/pca9685_simpletest.py from board import SCL, SDA import busio from adafruit_pca9685 import PCA9685 except ModuleNotFoundError: return try: i2c_bus = busio.I2C(SCL, SDA) self.controller = PCA9685(i2c_bus) self.controller.frequency = 60 self.initialized = True redis.put("strip_initialized", True) except ValueError: # LED strip is not connected return
def __init__(self, manager) -> None: super().__init__(manager, "ring") try: import rpi_ws281x except ModuleNotFoundError: return self.controller = rpi_ws281x.Adafruit_NeoPixel( self.LED_COUNT, self.LED_PIN, self.LED_FREQ_HZ, self.LED_DMA, self.LED_INVERT, self.LED_BRIGHTNESS, self.LED_CHANNEL, ) try: self.controller.begin() self.initialized = True redis.put("ring_initialized", True) except RuntimeError: # could not connect to led ring return
def _decorator(request: WSGIRequest) -> HttpResponse: # create a sessions if none exists (necessary for anonymous users) if not request.session or not request.session.session_key: request.session.save() request_ip = get_client_ip(request) last_requests = redis.get("last_requests") last_requests[request_ip] = time.time() redis.put("last_requests", last_requests) def check(): active = redis.get("active_requests") if active > 0: leds.enable_act_led() else: leds.disable_act_led() redis.connection.incr("active_requests") check() response = func(request) redis.connection.decr("active_requests") check() return response
def start() -> None: """Initializes this module by checking which platforms are available to use.""" # local songs are enabled if a library is set local_enabled = os.path.islink(library.get_library_path()) storage.put("local_enabled", local_enabled) # in the docker container all dependencies are installed youtube_available = conf.DOCKER or importlib.util.find_spec( "yt_dlp") is not None redis.put("youtube_available", youtube_available) if not youtube_available: # if youtube is not available, overwrite the database to disable it storage.put("youtube_enabled", False) # Spotify has no python dependencies we could easily check. try: spotify_available = ( conf.DOCKER or "[spotify]" in subprocess.check_output( ["mopidy", "config"], stderr=subprocess.DEVNULL).decode().splitlines()) except FileNotFoundError: # mopidy is not installed (eg in docker). Since we can't check, enable spotify_available = True redis.put("spotify_available", spotify_available) if not spotify_available: storage.put("spotify_enabled", False) soundcloud_available = (conf.DOCKER or importlib.util.find_spec("soundcloud") is not None) redis.put("soundcloud_available", soundcloud_available) if not soundcloud_available: storage.put("soundcloud_enabled", False) # Jamendo has no python dependencies we could easily check. try: jamendo_available = ( conf.DOCKER or "[jamendo]" in subprocess.check_output( ["mopidy", "config"], stderr=subprocess.DEVNULL).decode().splitlines()) except FileNotFoundError: jamendo_available = True redis.put("jamendo_available", jamendo_available) if not jamendo_available: storage.put("jamendo_enabled", False)
def _check_internet() -> None: host = storage.get("connectivity_host") if not host: redis.put("has_internet", False) return response = subprocess.call( ["ping", "-c", "1", "-W", "3", host], stdout=subprocess.DEVNULL ) if response == 0: redis.put("has_internet", True) else: redis.put("has_internet", False)
def _set_scan_progress(scan_progress: str) -> None: redis.put("library_scan_progress", scan_progress) settings.update_state()
def _stop_bluetoothctl(bluetoothctl: subprocess.Popen) -> None: assert bluetoothctl.stdin bluetoothctl.stdin.close() bluetoothctl.wait() redis.put("bluetoothctl_active", False)
def skip(_request: WSGIRequest) -> None: """Skips the current song and continues with the next one.""" with playback.mopidy_command() as allowed: if allowed: redis.put("backup_playing", False) PLAYER.playback.next()