"""Device configuration for the Ekklesia Classroom shell. Everything that has to agree about /etc/ekklesia-classroom/device.conf reads it through here, so the dock, the notification daemon and the DVD commands cannot drift apart over what a setting means. """ import math from pathlib import Path CONFIG_FILE = Path("/etc/ekklesia-classroom/device.conf") TRUTHY = {"1", "true", "yes", "on", "enabled"} # The dock is tuned for a 1080p classroom TV at 1.0. Smaller rooms with a # 32" panel sit near the bottom of this range, a 65" set near the top. UI_SCALE_DEFAULT = 1.0 UI_SCALE_MIN = 0.8 UI_SCALE_MAX = 1.5 DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration UPDATE_URL=https://updates.riverofglorymn.org CHANNEL=stable # Show the Sanctuary LIVE placeholder in the dock. Live viewing is not # implemented yet; this only controls whether the current placeholder appears. LIVE_ENABLED=false # Which output the room plays through. # # auto prefer a connected HDMI or DisplayPort television, and fall # back to the analog jack. This is right for almost every room. # hdmi always the television, even if it does not report itself. # analog always the jack. # # auto re-decides whenever the hardware changes, so a television switched # on after the computer is picked up without anybody touching anything, # and sound that is already playing is moved across to it. AUDIO_OUTPUT=auto # Size of the dock, clock and notifications, 0.8 to 1.5. # Larger rooms and larger TVs want a larger number. UI_SCALE=1.0 # Church message broker. The room is this machine's hostname unless ROOM # is set. Leave MQTT_ENABLED=false until a broker exists. MQTT_ENABLED=false MQTT_HOST= MQTT_PORT=1883 MQTT_USERNAME= MQTT_PASSWORD= """ # Sections added after the first classroom release. Keeping them separate lets # an upgrade make a setting discoverable without replacing anything an # administrator has already put in device.conf. DEVICE_CONFIG_SECTIONS = ( ( "LIVE_ENABLED", """# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not # implemented yet; this only controls whether the current placeholder appears. LIVE_ENABLED=false """, ), ( "AUDIO_OUTPUT", """# Which output the room plays through. # # auto prefer a connected HDMI or DisplayPort television, and fall # back to the analog jack. This is right for almost every room. # hdmi always the television, even if it does not report itself. # analog always the jack. # # auto re-decides whenever the hardware changes, so a television switched # on after the computer is picked up without anybody touching anything, # and sound that is already playing is moved across to it. AUDIO_OUTPUT=auto """, ), ( "UI_SCALE", """# Size of the dock, clock and notifications, 0.8 to 1.5. # Larger rooms and larger TVs want a larger number. UI_SCALE=1.0 """, ), ( "MQTT_ENABLED", """# Church message broker. The room is this machine's hostname unless ROOM # is set. Leave MQTT_ENABLED=false until a broker exists. MQTT_ENABLED=false MQTT_HOST= MQTT_PORT=1883 MQTT_USERNAME= MQTT_PASSWORD= """, ), ) def read_device_config(path=None): config = {} path = Path(path) if path is not None else CONFIG_FILE if not path.exists(): return config for raw_line in path.read_text().splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) config[key.strip()] = value.strip() return config def enabled(config, key, default=False): if key not in config: return default return config[key].lower() in TRUTHY def ensure_device_config(path=None): """Create device.conf or append any missing settings exactly once. Existing bytes are otherwise left alone: per-room settings, comments and even keys this release does not know about all survive an upgrade. """ path = Path(path) if path is not None else CONFIG_FILE if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(DEVICE_CONFIG_TEMPLATE) return text = path.read_text() config = read_device_config(path) additions = [] for key, section in DEVICE_CONFIG_SECTIONS: if key not in config: additions.append(section.rstrip("\n")) if not additions: return separator = "" if not text or text.endswith("\n") else "\n" path.write_text(text + separator + "\n\n".join(additions) + "\n") def ui_scale(config): """UI_SCALE, clamped to something a classroom can actually use.""" raw = config.get("UI_SCALE", "") try: value = float(raw) except (TypeError, ValueError): return UI_SCALE_DEFAULT if not math.isfinite(value): return UI_SCALE_DEFAULT return min(max(value, UI_SCALE_MIN), UI_SCALE_MAX) # The scale the classroom's own GTK surfaces render at. # # UI_SCALE is applied to the whole output (see classroom-scale), # which scales Chrome, VLC and every other application along with the # panel. Our surfaces are measured in the same logical pixels the # compositor is # already scaling, so scaling them here as well would apply UI_SCALE # twice and the dock would come out half again too big. # # This is the seam: if compositor scaling ever has to be abandoned, # changing this back to ui_scale(config) restores per-surface scaling # without touching the templates. SHELL_SCALE = 1.0 def scaled(value, scale): """A pixel measurement at the given scale, never smaller than 1.""" return max(1, int(round(value * scale)))