diff --git a/.github/workflows/django-checks.yml b/.github/workflows/django-checks.yml new file mode 100644 index 00000000..76443d07 --- /dev/null +++ b/.github/workflows/django-checks.yml @@ -0,0 +1,66 @@ +name: "Django checks" + +on: + pull_request: + paths: + - "docker/web/**" + - "docker/ruff.toml" + - ".github/workflows/django-checks.yml" + push: + branches: + - "main" + - "devel" + - "beta" + paths: + - "docker/web/**" + - "docker/ruff.toml" + - ".github/workflows/django-checks.yml" + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # Pinned: ruff's default rules and its findings change between releases. + - name: Install ruff + run: pip install ruff==0.16.10 + + # Only undefined names and syntax errors fail the build for now. The rest of + # docker/ruff.toml's rules still report findings (unused imports and the like) + # that need a cleanup pass before they can be enforced. + - name: Check for undefined names and syntax errors + working-directory: docker + run: ruff check web/ --select F821,E9 --output-format github + + migrations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" # Matches the python:3.12 base image in docker/Dockerfile + cache: pip + cache-dependency-path: docker/web/requirements.txt + + - name: Install dependencies + run: pip install -r docker/web/requirements.txt + + # settings.py keeps the secret key and database in /data, as in the container. + - name: Create the data directory + run: sudo install -d -o "$USER" /data + + - name: Run Django system checks + working-directory: docker/web/nspanelmanager + run: python manage.py check + + # Fails if a model change has no migration, which would leave existing installs + # with a database that doesn't match the code after upgrading. + - name: Check for missing migrations + working-directory: docker/web/nspanelmanager + run: python manage.py makemigrations --check --dry-run diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..72be0366 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,86 @@ +name: "Docker build" + +# Builds the release image without pushing it, so a broken Dockerfile, dependency or cross-compile +# shows up on the pull request instead of when a release is published. Pull requests and pushes build +# amd64 and aarch64; the weekly run and manual runs also build the 32-bit platforms. + +on: + pull_request: + paths: + - "docker/**" + - ".github/workflows/docker-build.yml" + push: + branches: + - "main" + - "devel" + - "beta" + paths: + - "docker/**" + - ".github/workflows/docker-build.yml" + schedule: + - cron: "0 4 * * 1" + workflow_dispatch: + +jobs: + build: + name: Build ${{ matrix.arch }} + runs-on: ubuntu-latest + # Building the Conan dependencies from source takes about an hour when the cache is cold. + timeout-minutes: 150 + strategy: + fail-fast: false + matrix: + # Same architectures as release.yml and docker/config.yaml. + arch: ${{ fromJSON((github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '["amd64", "aarch64", "armv7", "armhf", "i386"]' || '["amd64", "aarch64"]') }} + steps: + - uses: actions/checkout@v4 + + - name: Map architecture to Docker platform + id: platform + run: | + case "${{ matrix.arch }}" in + amd64) echo "platform=linux/amd64" >> "$GITHUB_OUTPUT" ;; + aarch64) echo "platform=linux/arm64" >> "$GITHUB_OUTPUT" ;; + armv7) echo "platform=linux/arm/v7" >> "$GITHUB_OUTPUT" ;; + armhf) echo "platform=linux/arm/v6" >> "$GITHUB_OUTPUT" ;; + i386) echo "platform=linux/386" >> "$GITHUB_OUTPUT" ;; + *) echo "Unknown architecture ${{ matrix.arch }}"; exit 1 ;; + esac + + # MQTTManager is cross-compiled on the runner's platform, but the final stage runs the + # target platform's apt-get and pip, which needs emulation. + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + id: buildx + + - name: Cache Conan packages + id: conan-cache + uses: actions/cache@v4 + with: + path: conan-cache + key: docker-conan-${{ matrix.arch }}-${{ hashFiles('docker/MQTTManager/conanfile.py', 'docker/build_scripts/install_build_tools.sh') }} + restore-keys: docker-conan-${{ matrix.arch }}- + + # Moves the cached directory into the Dockerfile's Conan cache mount before the build, + # and back out afterwards so actions/cache can save it. + - name: Restore Conan cache mount + uses: reproducible-containers/buildkit-cache-dance@v3 + with: + builder: ${{ steps.buildx.outputs.name }} + cache-map: | + { + "conan-cache": { + "target": "/MQTTManager/conan_cache", + "id": "nspm-conan", + "sharing": "locked" + } + } + skip-extraction: ${{ steps.conan-cache.outputs.cache-hit }} + + - name: Build image + uses: docker/build-push-action@v6 + with: + context: docker + platforms: ${{ steps.platform.outputs.platform }} + push: false diff --git a/.github/workflows/protobuf.yml b/.github/workflows/protobuf.yml new file mode 100644 index 00000000..63cfb77c --- /dev/null +++ b/.github/workflows/protobuf.yml @@ -0,0 +1,72 @@ +name: "Protobuf" + +# The code generated from docker/protobuf/*.proto is committed. Check that it matches the .proto +# files, and that protoc and both protobuf runtimes are on the same release: the generated C++ +# refuses to compile, and the generated Python refuses to import, against a different version. + +on: + pull_request: + paths: + - "docker/protobuf/**" + - "docker/MQTTManager/include/protobuf/**" + - "docker/MQTTManager/conanfile.py" + - "docker/web/nspanelmanager/web/protobuf/**" + - "docker/web/requirements.txt" + - "protobuf_firmware/**" + - ".github/workflows/protobuf.yml" + push: + branches: + - "main" + - "devel" + - "beta" + paths: + - "docker/protobuf/**" + - "docker/MQTTManager/include/protobuf/**" + - "docker/MQTTManager/conanfile.py" + - "docker/web/nspanelmanager/web/protobuf/**" + - "docker/web/requirements.txt" + - "protobuf_firmware/**" + - ".github/workflows/protobuf.yml" + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check protobuf versions match + run: | + protoc_version=$(sed -nE 's/^REMOTE_FILE="protoc-([0-9.]+)-.*/\1/p' docker/protobuf/install_protobuf.sh) + conan_version=$(sed -nE 's/.*"protobuf\/([0-9.]+)".*/\1/p' docker/MQTTManager/conanfile.py) + python_version=$(sed -nE 's/^protobuf==([0-9.]+).*/\1/p' docker/web/requirements.txt) + echo "protoc $protoc_version, Conan protobuf $conan_version, Python protobuf $python_version" + # protoc 27.0 is the protoc of the 5.27.0 C++ and Python runtimes. + if [ -z "$protoc_version" ] || [ "5.$protoc_version" != "$conan_version" ] || [ "$conan_version" != "$python_version" ]; then + echo "::error::protoc ($protoc_version in install_protobuf.sh), Conan protobuf ($conan_version in conanfile.py) and Python protobuf ($python_version in requirements.txt) must be the same release" + exit 1 + fi + + - name: Install protoc and protoc-gen-c + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends protobuf-c-compiler + cd docker/protobuf && sudo bash install_protobuf.sh + protoc --version + + - name: Regenerate + working-directory: docker/protobuf + env: + DEST_DIR_MQTTMANAGER: ${{ github.workspace }}/docker/MQTTManager/include/protobuf/ + DEST_DIR_DJANGO: ${{ github.workspace }}/docker/web/nspanelmanager/web/protobuf/ + DEST_DIR_FIRMWARE: ${{ github.workspace }}/protobuf_firmware/ + # -e: compile_all.sh carries on past a failed protoc, which would leave the old files in place. + run: bash -e compile_all.sh + + - name: Check generated code is up to date + run: | + git add --intent-to-add --all + if ! git diff --exit-code --stat; then + git diff + echo "::error::Generated protobuf code is out of date. Run docker/protobuf/compile_all.sh and commit the result." + exit 1 + fi diff --git a/.github/workflows/react-components.yml b/.github/workflows/react-components.yml new file mode 100644 index 00000000..dd154021 --- /dev/null +++ b/.github/workflows/react-components.yml @@ -0,0 +1,56 @@ +name: "React components" + +# The React components in docker/web/nspanelmanager/web/react-components are built into dist/ and +# templates/scripts.html, which are committed and served as-is: the Docker image does not rebuild +# them. Check that they type-check, build, and match what is committed. + +on: + pull_request: + paths: + - "docker/web/nspanelmanager/web/react-components/**" + - ".github/workflows/react-components.yml" + push: + branches: + - "main" + - "devel" + - "beta" + paths: + - "docker/web/nspanelmanager/web/react-components/**" + - ".github/workflows/react-components.yml" + +defaults: + run: + working-directory: docker/web/nspanelmanager/web/react-components + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: docker/web/nspanelmanager/web/react-components/package-lock.json + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + # "build" runs tsc -b before vite build, so this is also the type check. + - name: Type-check and build + run: npm run build + + - name: Check the committed build is up to date + run: | + git add --intent-to-add --all . + if ! git diff --exit-code --stat -- .; then + echo "::error::dist/ or templates/scripts.html is out of date. Run the build in react-components and commit the result." + exit 1 + fi + + # Reported but not enforced yet: the existing code has 30 errors, mostly no-explicit-any and + # react-refresh/only-export-components. + - name: Lint + continue-on-error: true + run: npx eslint . diff --git a/docker/Dockerfile b/docker/Dockerfile index 94bd5a14..8eec6b57 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,15 @@ FROM --platform=$BUILDPLATFORM node:20-alpine AS ui-builder ARG BUILDTIME +# Commit of NSPanelManager-Web-UI to build. Pinned so that a given commit of this repo always builds +# the same UI; bump it to ship UI changes. Pass --build-arg WEB_UI_REF=main to build the latest. +ARG WEB_UI_REF=6f3eae63296e1875314641aa9f79eed54ae1408e WORKDIR /ui-app # Install git to clone your frontend repository RUN apk add --no-cache git # Clone the UI repo -RUN echo "Building UI at $BUILDTIME" && git clone https://github.com/NSPManager/NSPanelManager-Web-UI.git . # Use the echo to force a rebuild of this step. +RUN echo "Building UI at $BUILDTIME" && git clone https://github.com/NSPManager/NSPanelManager-Web-UI.git . && git checkout --detach "$WEB_UI_REF" # Use the echo to force a rebuild of this step. # Install dependencies and build RUN npm install @@ -29,7 +32,11 @@ COPY MQTTManager/ /MQTTManager/ # Only build MQTTManager during Docker build if is is not a devel mode. RUN if [ "$IS_DEVEL" != "yes" ]; then bash /build_scripts/install_build_tools.sh "${IS_DEVEL}"; fi -RUN if [ -z "$no_mqttmanager_build" ]; then /bin/bash /MQTTManager/compile_mqttmanager.sh --target-platform "$TARGETPLATFORM" --strip; else echo "Not building MQTTManager."; fi +# The Conan package cache (core.cache:storage_path in install_build_tools.sh) is a BuildKit cache +# mount, so rebuilds reuse the dependencies instead of compiling them from source again, and CI can +# save it between runs. Only /MQTTManager/build is copied out of this stage. +RUN --mount=type=cache,id=nspm-conan,target=/MQTTManager/conan_cache,sharing=locked \ + if [ -z "$no_mqttmanager_build" ]; then /bin/bash /MQTTManager/compile_mqttmanager.sh --target-platform "$TARGETPLATFORM" --strip; else echo "Not building MQTTManager."; fi FROM python:3.12.5-bookworm ARG no_mqttmanager_build diff --git a/docker/protobuf/compile_all.sh b/docker/protobuf/compile_all.sh index 3d49134c..4decf61a 100755 --- a/docker/protobuf/compile_all.sh +++ b/docker/protobuf/compile_all.sh @@ -1,7 +1,8 @@ #!/bin/bash -DEST_DIR_MQTTMANAGER=/MQTTManager/include/protobuf/ -DEST_DIR_DJANGO=/usr/src/app/nspanelmanager/web/protobuf/ -DEST_DIR_FIRMWARE=/full_git/protobuf_firmware/ +# Output directories default to their paths in the dev container; CI overrides them. +DEST_DIR_MQTTMANAGER=${DEST_DIR_MQTTMANAGER:-/MQTTManager/include/protobuf/} +DEST_DIR_DJANGO=${DEST_DIR_DJANGO:-/usr/src/app/nspanelmanager/web/protobuf/} +DEST_DIR_FIRMWARE=${DEST_DIR_FIRMWARE:-/full_git/protobuf_firmware/} SRC_FILES=('protobuf_mqttmanager.proto' 'protobuf_general.proto' 'protobuf_formats.proto') for SRC_FILE in ${SRC_FILES[@]}; do diff --git a/docker/ruff.toml b/docker/ruff.toml index b0e205b5..ec86d4e2 100644 --- a/docker/ruff.toml +++ b/docker/ruff.toml @@ -1,7 +1,12 @@ # Allow lines to be as long as 120. line-length = 320 +# Migrations and protobuf modules are generated. +extend-exclude = ["web/nspanelmanager/web/migrations", "web/nspanelmanager/web/protobuf"] [lint] +# Ruff's long-standing defaults, listed explicitly so that a newer ruff +# doesn't enable more rules on its own. +select = ["E4", "E7", "E9", "F"] # Avoid enforcing line-length violations (`E501`) ignore = ["E501"] diff --git a/docker/web/nspanelmanager/web/htmx.py b/docker/web/nspanelmanager/web/htmx.py index 7a4ceac5..30f18416 100644 --- a/docker/web/nspanelmanager/web/htmx.py +++ b/docker/web/nspanelmanager/web/htmx.py @@ -270,20 +270,6 @@ def relay_group_remove_relay(request, relay_binding_id): return response -@csrf_exempt -def handle_entity_modal_result(request): - if request.session["action"] == "ADD_SCENE_TO_NSPANEL_ENTITY_PAGE": - return create_or_update_scene_entity(request) - else: - return JsonResponse( - { - "status": "error", - "text": "Unknown action! Action: " + request.session["action"], - }, - status=500, - ) - - def partial_select_new_entity_item_list(request, action, action_args): # This is used in the last step of adding an entity to call the correct # "add function" so that it get added to the correct room, page and so on. @@ -454,35 +440,6 @@ def partial_entity_add_scene_entity(request): return render(request, "partial/select_entity/entity_add_or_edit_scene.html", data) -@csrf_exempt -def partial_remove_entity_from_page_slot(request, page_id, slot_id): - page = RoomEntitiesPage.objects.get(id=page_id) - - # Check for light in given slot - entities = page.entity_set.filter(room_view_position=slot_id).all() - if entities.count() > 0: - entities.delete() - send_mqttmanager_reload_command() - - entities = page.scene_set.filter(room_view_position=slot_id).all() - if entities.count() > 0: - entities.delete() - send_mqttmanager_reload_command() - - room_id = 0 - if page.room: - room_id = page.room.id - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=room_id, - is_scenes_pages=page.is_scenes_page, - is_global_scenes_page=(page.room is None), - ) - - def get_entity_in_page_slot(page_id, slot_id): page = RoomEntitiesPage.objects.get(id=page_id) entities = Entity.objects.filter(entities_page=page, room_view_position=slot_id) @@ -496,98 +453,6 @@ def get_entity_in_page_slot(page_id, slot_id): return None -@csrf_exempt -def partial_move_entity(request): - existing_entity_in_slot = get_entity_in_page_slot(request.POST["page_id"], request.POST["slot_id"]) - new_entity_in_slot = None - if request.POST["new_entity_type"] == "scene": - new_entity_in_slot = Scene.objects.get(id=request.POST["new_entity_id"]) - else: - try: - new_entity_in_slot = Entity.objects.get(id=request.POST["new_entity_id"]) - except Exception as e: - return JsonResponse({"status": "error", "text": "Did not find existing entity to move!"}) - - if existing_entity_in_slot: - # Swap the existing entity place with the new entity to be put on that slot - existing_entity_in_slot.entities_page = new_entity_in_slot.entities_page - existing_entity_in_slot.room_view_position = new_entity_in_slot.room_view_position - existing_entity_in_slot.save() - - new_entity_in_slot.entities_page = RoomEntitiesPage.objects.get(id=request.POST["page_id"]) - new_entity_in_slot.room_view_position = request.POST["slot_id"] - new_entity_in_slot.save() - send_mqttmanager_reload_command() - - if new_entity_in_slot.room is None: - room_id = 0 - is_global_scenes_page = True - else: - room_id = new_entity_in_slot.room.id - is_global_scenes_page = False - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request, - view="edit_room", - room_id=room_id, - is_scenes_pages=new_entity_in_slot.entities_page.is_scenes_page, - is_global_scenes_page=is_global_scenes_page, - ) - - -@csrf_exempt -def partial_move_entities_pages(request): - if "htmx_form_save_entities_pages_order_field" in request.POST: - json_data = json.loads(request.POST["htmx_form_save_entities_pages_order_field"]) - if "pages" in json_data: - if len(json_data["pages"]) > 0: - entity_page = RoomEntitiesPage.objects.get(id=json_data["pages"][0]) - is_global_scenes_pages = False - if entity_page.room is None: - room_id = 0 - is_global_scenes_pages = True - else: - room_id = entity_page.room.id - for index, page_id in enumerate(json_data["pages"]): - page = RoomEntitiesPage.objects.get(id=page_id) - page.display_order = index - page.save() - send_mqttmanager_reload_command() - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request, - view="edit_room", - room_id=room_id, - is_scenes_pages=entity_page.is_scenes_page, - is_global_scenes_page=is_global_scenes_pages, - ) - else: - return JsonResponse( - { - "status": "error", - "text": "'pages' field empty in request POST-data.", - }, - status=500, - ) - else: - return JsonResponse( - { - "status": "error", - "text": "'pages' field not available in JSON-data.", - }, - status=500, - ) - else: - return JsonResponse( - { - "status": "error", - "text": "'htmx_form_save_entities_pages_order_field' field not available in request POST-data.", - }, - status=500, - ) - - @csrf_exempt def partial_add_entity_to_entities_page_select_entity_type_react(request, entities_page_id, room_view_position): entities_page = RoomEntitiesPage.objects.get(id=entities_page_id) @@ -652,74 +517,6 @@ def partial_add_entity_to_entities_page_config_modal(request, entity_source): ) -@csrf_exempt -def partial_delete_entities_page(request, page_id): - page = RoomEntitiesPage.objects.get(id=page_id) - if page.room is None: - is_global_scenes_page = True - room_id = 0 - else: - is_global_scenes_page = False - room_id = page.room.id - page.delete() - - # Recalculate entity page order - for index, entity_page in enumerate( - RoomEntitiesPage.objects.filter(room=page.room, is_scenes_page=page.is_scenes_page).order_by("display_order"), - start=0, - ): - entity_page.display_order = index - entity_page.save() - - send_mqttmanager_reload_command() - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=room_id, - is_scenes_pages=page.is_scenes_page, - is_global_scenes_page=is_global_scenes_page, - ) - - -def create_entities_page_in_room(request, room_id, page_type, is_scenes_page, is_global_scenes_page): - entity_page = RoomEntitiesPage() - entity_page.is_scenes_page = is_scenes_page == "True" - entity_page.is_global_scenes_page = is_global_scenes_page == "True" - if entity_page.is_global_scenes_page: - entity_page.room = None - entity_page.display_order = RoomEntitiesPage.objects.filter(room=None, is_scenes_page=is_scenes_page).count() - else: - room = Room.objects.get(id=room_id) - entity_page.room = room - entity_page.display_order = RoomEntitiesPage.objects.filter(room=room, is_scenes_page=is_scenes_page).count() - - if page_type == 4: - entity_page.page_type = 4 - entity_page.save() - send_mqttmanager_reload_command() - elif page_type == 8: - entity_page.page_type = 8 - entity_page.save() - send_mqttmanager_reload_command() - elif page_type == 12: - entity_page.page_type = 12 - entity_page.save() - send_mqttmanager_reload_command() - else: - print(f"ERROR! Unknown page type {page_type}") - # Return new partial HTMX update of all entities pages in this room - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=room_id, - is_scenes_pages=entity_page.is_scenes_page, - is_global_scenes_page=(entity_page.room == None), - ) - - @csrf_exempt def partial_reorder_rooms(request): if "htmx_form_save_rooms_order_field" in request.POST: @@ -772,200 +569,6 @@ def partial_select_new_outside_temperature_sensor(request): # When creating a new or updating an existing light entity this will take care of the actual creation/updating of the model # in the database. -def create_or_update_light_entity(request): - action_args = json.loads(request.session["action_args"]) # Loads arguments set when first starting process of adding/updating entity - - entity_data = { - "controller": request.session["entity_source"], - "home_assistant_name": "", - "openhab_name": "", - "openhab_control_mode": "", - "openhab_item_switch": "", - "openhab_item_dimmer": "", - "openhab_item_color_temp": "", - "openhab_item_rgb": "", - "can_dim": False, - "can_color_temperature": False, - "can_rgb": False, - "is_ceiling_light": False, - "controlled_by_nspanel_main_page": True, - } - if "entity_id" in action_args and int(action_args["entity_id"]) >= 0: - new_light = Entity.objects.get(id=int(action_args["entity_id"])) - entity_data = new_light.entity_data - else: - new_light = Entity() - new_light.entity_type = Entity.EntityType.LIGHT - - if entity_data["controller"] == "home_assistant": - entity_data["home_assistant_name"] = request.POST["home_assistant_item"] - new_light.friendly_name = request.POST["add_new_light_name"] - new_light.room = Room.objects.get(id=int(action_args["room_id"])) - new_light.entities_page = RoomEntitiesPage.objects.get(id=int(action_args["page_id"])) - new_light.room_view_position = int(action_args["page_slot"]) - - entity_data["controlled_by_nspanel_main_page"] = "controlled_by_nspanel_main_page" in request.POST - entity_data["is_ceiling_light"] = request.POST["light_type"] == "ceiling" - - if request.POST["light_control_mode"] == "dimmer": - entity_data["can_dim"] = True - entity_data["openhab_control_mode"] = "dimmer" - if entity_data["controller"] == "openhab": - entity_data["openhab_item_dimmer"] = request.POST["openhab_dimming_item"] - else: - entity_data["openhab_control_mode"] = "switch" - entity_data["can_dim"] = False - if entity_data["controller"] == "openhab": - entity_data["openhab_item_switch"] = request.POST["openhab_dimming_item"] - - if "color_temperature" in request.POST: - entity_data["can_color_temperature"] = True - if entity_data["controller"] == "openhab": - entity_data["openhab_item_color_temp"] = request.POST["openhab_color_temperature_item"] - else: - entity_data["can_color_temperature"] = False - entity_data["openhab_item_color_temp"] = "" - - if "rgb" in request.POST: - entity_data["can_rgb"] = True - if entity_data["controller"] == "openhab": - entity_data["openhab_item_rgb"] = request.POST["openhab_rgb_item"] - else: - entity_data["can_rgb"] = False - entity_data["openhab_item_rgb"] = "" - - new_light.entity_data = entity_data - new_light.save() - send_mqttmanager_reload_command() - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=new_light.room.id, - is_scenes_pages=False, - is_global_scenes_page=False, - ) - - -def create_or_update_switch_entity(request): - action_args = json.loads(request.session["action_args"]) # Loads arguments set when first starting process of adding/updating entity - - entity_data = { - "controller": request.session["entity_source"], - "home_assistant_name": "", - "openhab_name": "", - "openhab_item_switch": "", - } - if "entity_id" in action_args and int(action_args["entity_id"]) >= 0: - new_switch = Entity.objects.get(id=int(action_args["entity_id"])) - entity_data = new_switch.entity_data - else: - new_switch = Entity() - new_switch.entity_type = Entity.EntityType.SWITCH - # Only set once, during initial creation: - new_switch.room = Room.objects.get(id=int(action_args["room_id"])) - new_switch.entities_page = RoomEntitiesPage.objects.get(id=int(action_args["page_id"])) - new_switch.room_view_position = int(action_args["page_slot"]) - if entity_data["controller"] == "home_assistant": - entity_data["home_assistant_name"] = request.POST["backend_name"] - elif entity_data["controller"] == "openhab": - entity_data["openhab_item_switch"] = request.POST["backend_name"] - - new_switch.friendly_name = request.POST["light_name"] - new_switch.entity_data = entity_data - new_switch.save() - send_mqttmanager_reload_command() - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=new_switch.room.id, - is_scenes_pages=False, - is_global_scenes_page=False, - ) - - -def create_or_update_button_entity(request): - action_args = json.loads(request.session["action_args"]) # Loads arguments set when first starting process of adding/updating entity - - entity_data = { - "controller": request.session["entity_source"], - "home_assistant_name": "", - "mqtt_topic": "", # Used in case of controller = manual - "mqtt_payload": "", # Used in case of controller = manual - } - if "entity_id" in action_args and int(action_args["entity_id"]): - new_button = Entity.objects.get(id=int(action_args["entity_id"])) - entity_data = new_button.entity_data - else: - new_button = Entity() - new_button.entity_type = Entity.EntityType.BUTTON - # Only set once, during initial creation: - new_button.room = Room.objects.get(id=int(action_args["room_id"])) - new_button.entities_page = RoomEntitiesPage.objects.get(id=int(action_args["page_id"])) - new_button.room_view_position = int(action_args["page_slot"]) - - if entity_data["controller"] == "home_assistant": - entity_data["home_assistant_name"] = request.POST["backend_name"] - elif entity_data["controller"] == "nspm": - entity_data["mqtt_topic"] = request.POST["mqtt_topic"] - entity_data["mqtt_payload"] = request.POST["mqtt_payload"] - new_button.friendly_name = request.POST["light_name"] - new_button.entity_data = entity_data - new_button.save() - send_mqttmanager_reload_command() - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=new_button.room.id, - is_scenes_pages=False, - is_global_scenes_page=False, - ) - - -def create_or_update_scene_entity(request): - action_args = json.loads(request.session["action_args"]) # Loads arguments set when first starting process of adding/updating entity - - if "entity_id" in action_args and int(action_args["entity_id"]) >= 0: - new_scene = Scene.objects.get(id=int(action_args["entity_id"])) - else: - new_scene = Scene() - if "room_id" in action_args: - new_scene.room = Room.objects.get(id=int(action_args["room_id"])) - else: - new_scene.room = None - new_scene.entities_page = RoomEntitiesPage.objects.get(id=int(action_args["page_id"])) - new_scene.room_view_position = int(action_args["page_slot"]) - new_scene.scene_type = request.session["entity_source"] - if new_scene.scene_type == "nspanelmanager": - pass # Do nothing, this is one of our own scenes. - elif new_scene.scene_type == "home_assistant": - new_scene.backend_name = request.POST["backend_name"] - elif new_scene.scene_type == "openhab": - new_scene.backend_name = request.POST["backend_name"] - - new_scene.friendly_name = request.POST["scene_name"] - new_scene.save() - send_mqttmanager_reload_command() - - room_id = 0 - if new_scene.room: - room_id = new_scene.room.id - - entities_pages = NSPanelRoomEntitiesPages() - return entities_pages.get( - request=request, - view="edit_room", - room_id=room_id, - is_scenes_pages=True, - is_global_scenes_page=(new_scene.room == None), - ) - - def show_messages(request): data = get_base_data(request) data["messages"] = Message.objects.all() diff --git a/docker/web/nspanelmanager/web/rest.py b/docker/web/nspanelmanager/web/rest.py index 6f3a7f1c..d394bdae 100644 --- a/docker/web/nspanelmanager/web/rest.py +++ b/docker/web/nspanelmanager/web/rest.py @@ -679,7 +679,7 @@ def scenes_post(request): scene.lightstate_set.all().delete() # Remove all old states # TODO: Convert light states to JSON fields in DB. for light_state in data["light_states"]: - light = Light.objects.filter(id=light_state["light_id"]).first() + light = Entity.objects.filter(id=light_state["light_id"], entity_type=Entity.EntityType.LIGHT).first() if light: new_state = LightState() new_state.light = light diff --git a/docker/web/nspanelmanager/web/urls.py b/docker/web/nspanelmanager/web/urls.py index 306874a7..4e956e55 100644 --- a/docker/web/nspanelmanager/web/urls.py +++ b/docker/web/nspanelmanager/web/urls.py @@ -100,7 +100,6 @@ ### HTMX "API" URLs ### ####################### path("htmx/unblock_nspanel/", htmx.unblock_nspanel, name="htmx_unblock_nspanel"), - path("htmx/partial/handle_entity_modal_result", htmx.handle_entity_modal_result, name="htmx_handle_entity_modal_result"), path("htmx/partial/select_new_entity_type_react//", htmx.partial_add_entity_to_entities_page_select_entity_type_react, name="htmx_partial_select_new_entity_type_react"), path("htmx/partial/select_new_entity_type//", htmx.partial_add_entity_to_entities_page_select_entity_type, name="htmx_partial_select_new_entity_type"), path("htmx/partial/select_new_entity_source//", htmx.partial_add_entity_to_entities_page_select_entity_source, name="htmx_partial_select_new_entity_source"), @@ -110,11 +109,6 @@ path("htmx/partial/add_light_entity_to_room/", htmx.partial_entity_add_light_entity, name="htmx_partial_add_light_entity"), path("htmx/partial/edit_light_entity_in_room/", htmx.partial_entity_edit_light_entity, name="htmx_partial_edit_light_entity"), path("htmx/partial/edit_scene_entity/", htmx.partial_entity_edit_scene_entity, name="htmx_partial_edit_scene_entity"), - path("htmx/partial/remove_entity_from_page_slot//", htmx.partial_remove_entity_from_page_slot, name="htmx_partial_remove_entity_from_page_slot"), - path("htmx/partial/move_entity", htmx.partial_move_entity, name="htmx_partial_move_entity"), - path("htmx/partial/reorder_entity_pages", htmx.partial_move_entities_pages, name="htmx_partial_move_entities_pages"), - path("htmx/partial/delete_entities_page/", htmx.partial_delete_entities_page, name="htmx_partial_delete_entities_page"), - path("htmx/partial/create_entities_page_in_room////", htmx.create_entities_page_in_room, name="htmx_create_entities_page_in_room"), path("htmx/partial/reorder_rooms", htmx.partial_reorder_rooms, name="htmx_partial_reorder_rooms"), path("htmx/partial/select_room_temperature_sensor_from_list//", htmx.select_room_temperature_sensor_from_list, name="htmx_partial_select_room_temperature_sensor_from_list"), path("htmx/partial/select_room_temperature_sensor_provider/", htmx.select_room_temperature_sensor_provider, name="htmx_partial_select_room_temperature_sensor_provider"), diff --git a/protobuf_firmware/protobuf_nspanel.pb-c.c b/protobuf_firmware/protobuf_nspanel.pb-c.c index 700dd92f..78a0ac12 100644 --- a/protobuf_firmware/protobuf_nspanel.pb-c.c +++ b/protobuf_firmware/protobuf_nspanel.pb-c.c @@ -13,6 +13,12 @@ void nspanel_config__room_info__init static const NSPanelConfig__RoomInfo init_value = NSPANEL_CONFIG__ROOM_INFO__INIT; *message = init_value; } +void nspanel_config__sound__init + (NSPanelConfig__Sound *message) +{ + static const NSPanelConfig__Sound init_value = NSPANEL_CONFIG__SOUND__INIT; + *message = init_value; +} void nspanel_config__init (NSPanelConfig *message) { @@ -458,6 +464,57 @@ const ProtobufCMessageDescriptor nspanel_config__room_info__descriptor = (ProtobufCMessageInit) nspanel_config__room_info__init, NULL,NULL,NULL /* reserved[123] */ }; +static const ProtobufCFieldDescriptor nspanel_config__sound__field_descriptors[2] = +{ + { + "event", + 1, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_ENUM, + 0, /* quantifier_offset */ + offsetof(NSPanelConfig__Sound, event), + &nspanel_config__nspanel_sound_event__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, + { + "rtttl", + 2, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_STRING, + 0, /* quantifier_offset */ + offsetof(NSPanelConfig__Sound, rtttl), + NULL, + &protobuf_c_empty_string, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, +}; +static const unsigned nspanel_config__sound__field_indices_by_name[] = { + 0, /* field[0] = event */ + 1, /* field[1] = rtttl */ +}; +static const ProtobufCIntRange nspanel_config__sound__number_ranges[1 + 1] = +{ + { 1, 0 }, + { 0, 2 } +}; +const ProtobufCMessageDescriptor nspanel_config__sound__descriptor = +{ + PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, + "NSPanelConfig.Sound", + "Sound", + "NSPanelConfig__Sound", + "", + sizeof(NSPanelConfig__Sound), + 2, + nspanel_config__sound__field_descriptors, + nspanel_config__sound__field_indices_by_name, + 1, nspanel_config__sound__number_ranges, + (ProtobufCMessageInit) nspanel_config__sound__init, + NULL,NULL,NULL /* reserved[123] */ +}; static const ProtobufCEnumValue nspanel_config__nspanel_default_page__enum_values_by_number[3] = { { "HOME", "NSPANEL_CONFIG__NSPANEL_DEFAULT_PAGE__HOME", 0 }, @@ -556,7 +613,37 @@ const ProtobufCEnumDescriptor nspanel_config__nspanel_button_mode__descriptor = nspanel_config__nspanel_button_mode__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; -static const ProtobufCFieldDescriptor nspanel_config__field_descriptors[36] = +static const ProtobufCEnumValue nspanel_config__nspanel_sound_event__enum_values_by_number[3] = +{ + { "TOUCH", "NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__TOUCH", 0 }, + { "SCENE_ACTIVATED", "NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__SCENE_ACTIVATED", 1 }, + { "SCENE_SAVED", "NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__SCENE_SAVED", 2 }, +}; +static const ProtobufCIntRange nspanel_config__nspanel_sound_event__value_ranges[] = { +{0, 0},{0, 3} +}; +static const ProtobufCEnumValueIndex nspanel_config__nspanel_sound_event__enum_values_by_name[3] = +{ + { "SCENE_ACTIVATED", 1 }, + { "SCENE_SAVED", 2 }, + { "TOUCH", 0 }, +}; +const ProtobufCEnumDescriptor nspanel_config__nspanel_sound_event__descriptor = +{ + PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC, + "NSPanelConfig.NSPanelSoundEvent", + "NSPanelSoundEvent", + "NSPanelConfig__NSPanelSoundEvent", + "", + 3, + nspanel_config__nspanel_sound_event__enum_values_by_number, + 3, + nspanel_config__nspanel_sound_event__enum_values_by_name, + 1, + nspanel_config__nspanel_sound_event__value_ranges, + NULL,NULL,NULL,NULL /* reserved[1234] */ +}; +static const ProtobufCFieldDescriptor nspanel_config__field_descriptors[37] = { { "name", @@ -990,6 +1077,18 @@ static const ProtobufCFieldDescriptor nspanel_config__field_descriptors[36] = 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, + { + "sounds", + 45, + PROTOBUF_C_LABEL_REPEATED, + PROTOBUF_C_TYPE_MESSAGE, + offsetof(NSPanelConfig, n_sounds), + offsetof(NSPanelConfig, sounds), + &nspanel_config__sound__descriptor, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, }; static const unsigned nspanel_config__field_indices_by_name[] = { 32, /* field[32] = button1_lower_temperature */ @@ -1024,6 +1123,7 @@ static const unsigned nspanel_config__field_indices_by_name[] = { 10, /* field[10] = screensaver_mode */ 11, /* field[11] = show_screensaver_inside_temperature */ 12, /* field[12] = show_screensaver_outside_temperature */ + 36, /* field[36] = sounds */ 7, /* field[7] = special_mode_release_time */ 6, /* field[6] = special_mode_trigger_time */ 20, /* field[20] = temperature_calibration */ @@ -1036,7 +1136,7 @@ static const ProtobufCIntRange nspanel_config__number_ranges[5 + 1] = { 30, 23 }, { 35, 27 }, { 37, 28 }, - { 0, 36 } + { 0, 37 } }; const ProtobufCMessageDescriptor nspanel_config__descriptor = { @@ -1046,7 +1146,7 @@ const ProtobufCMessageDescriptor nspanel_config__descriptor = "NSPanelConfig", "", sizeof(NSPanelConfig), - 36, + 37, nspanel_config__field_descriptors, nspanel_config__field_indices_by_name, 5, nspanel_config__number_ranges, diff --git a/protobuf_firmware/protobuf_nspanel.pb-c.h b/protobuf_firmware/protobuf_nspanel.pb-c.h index 54158df8..013a2092 100644 --- a/protobuf_firmware/protobuf_nspanel.pb-c.h +++ b/protobuf_firmware/protobuf_nspanel.pb-c.h @@ -17,6 +17,7 @@ PROTOBUF_C__BEGIN_DECLS typedef struct NSPanelConfig NSPanelConfig; typedef struct NSPanelConfig__RoomInfo NSPanelConfig__RoomInfo; +typedef struct NSPanelConfig__Sound NSPanelConfig__Sound; typedef struct NSPanelWarning NSPanelWarning; typedef struct NSPanelStatusReport NSPanelStatusReport; typedef struct NSPanelRoomEntitiesPage NSPanelRoomEntitiesPage; @@ -67,6 +68,25 @@ typedef enum _NSPanelConfig__NSPanelButtonMode { NSPANEL_CONFIG__NSPANEL_BUTTON_MODE__THERMOSTAT_COOL = 4 PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(NSPANEL_CONFIG__NSPANEL_BUTTON_MODE) } NSPanelConfig__NSPanelButtonMode; +/* + * Sounds played by the panel buzzer, one entry per event. An event without an entry, or with an empty rtttl, is silent. + * Panels ignore events they do not know about. + */ +typedef enum _NSPanelConfig__NSPanelSoundEvent { + /* + * The screen was touched + */ + NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__TOUCH = 0, + /* + * A scene was triggered from the panel. Played instead of the TOUCH sound for that touch. + */ + NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__SCENE_ACTIVATED = 1, + /* + * A scene was saved from the panel + */ + NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__SCENE_SAVED = 2 + PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(NSPANEL_CONFIG__NSPANEL_SOUND_EVENT) +} NSPanelConfig__NSPanelSoundEvent; typedef enum _NSPanelStatusReport__State { NSPANEL_STATUS_REPORT__STATE__ONLINE = 0, NSPANEL_STATUS_REPORT__STATE__OFFLINE = 1, @@ -126,6 +146,20 @@ struct NSPanelConfig__RoomInfo , 0, 0,NULL, 0,NULL } +struct NSPanelConfig__Sound +{ + ProtobufCMessage base; + NSPanelConfig__NSPanelSoundEvent event; + /* + * Melody in RTTTL format, ex. "touch:d=32,o=6,b=190:g6,16p,32c7" + */ + char *rtttl; +}; +#define NSPANEL_CONFIG__SOUND__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&nspanel_config__sound__descriptor) \ + , NSPANEL_CONFIG__NSPANEL_SOUND_EVENT__TOUCH, (char *)protobuf_c_empty_string } + + struct NSPanelConfig { ProtobufCMessage base; @@ -190,10 +224,12 @@ struct NSPanelConfig * At what temperature should relay2 turn off (heating) or on (cooling) when in thermostat mode. */ int32_t button2_upper_temperature; + size_t n_sounds; + NSPanelConfig__Sound **sounds; }; #define NSPANEL_CONFIG__INIT \ { PROTOBUF_C_MESSAGE_INIT (&nspanel_config__descriptor) \ - , (char *)protobuf_c_empty_string, 0, NSPANEL_CONFIG__NSPANEL_DEFAULT_PAGE__HOME, 0, 0, 0, 0, 0, 0, 0, NSPANEL_CONFIG__NSPANEL_SCREENSAVER_MODE__WEATHER_WITH_BACKGROUND, 0, 0, 0, 0, 0, 0,NULL, 0, 0, 0, 0, NSPANEL_CONFIG__NSPANEL_BUTTON_MODE__DIRECT, NSPANEL_CONFIG__NSPANEL_BUTTON_MODE__DIRECT, 0,NULL, 0, 0, 0, 0,NULL, 0,NULL, 0, 0, (char *)protobuf_c_empty_string, 0, 0, 0, 0 } + , (char *)protobuf_c_empty_string, 0, NSPANEL_CONFIG__NSPANEL_DEFAULT_PAGE__HOME, 0, 0, 0, 0, 0, 0, 0, NSPANEL_CONFIG__NSPANEL_SCREENSAVER_MODE__WEATHER_WITH_BACKGROUND, 0, 0, 0, 0, 0, 0,NULL, 0, 0, 0, 0, NSPANEL_CONFIG__NSPANEL_BUTTON_MODE__DIRECT, NSPANEL_CONFIG__NSPANEL_BUTTON_MODE__DIRECT, 0,NULL, 0, 0, 0, 0,NULL, 0,NULL, 0, 0, (char *)protobuf_c_empty_string, 0, 0, 0, 0, 0,NULL } struct NSPanelWarning @@ -509,6 +545,9 @@ struct NSPanelMQTTManagerCommand /* NSPanelConfig__RoomInfo methods */ void nspanel_config__room_info__init (NSPanelConfig__RoomInfo *message); +/* NSPanelConfig__Sound methods */ +void nspanel_config__sound__init + (NSPanelConfig__Sound *message); /* NSPanelConfig methods */ void nspanel_config__init (NSPanelConfig *message); @@ -680,6 +719,9 @@ void nspanel_mqttmanager_command__free_unpacked typedef void (*NSPanelConfig__RoomInfo_Closure) (const NSPanelConfig__RoomInfo *message, void *closure_data); +typedef void (*NSPanelConfig__Sound_Closure) + (const NSPanelConfig__Sound *message, + void *closure_data); typedef void (*NSPanelConfig_Closure) (const NSPanelConfig *message, void *closure_data); @@ -743,9 +785,11 @@ typedef void (*NSPanelMQTTManagerCommand_Closure) extern const ProtobufCEnumDescriptor nspanel_warning_level__descriptor; extern const ProtobufCMessageDescriptor nspanel_config__descriptor; extern const ProtobufCMessageDescriptor nspanel_config__room_info__descriptor; +extern const ProtobufCMessageDescriptor nspanel_config__sound__descriptor; extern const ProtobufCEnumDescriptor nspanel_config__nspanel_default_page__descriptor; extern const ProtobufCEnumDescriptor nspanel_config__nspanel_screensaver_mode__descriptor; extern const ProtobufCEnumDescriptor nspanel_config__nspanel_button_mode__descriptor; +extern const ProtobufCEnumDescriptor nspanel_config__nspanel_sound_event__descriptor; extern const ProtobufCMessageDescriptor nspanel_warning__descriptor; extern const ProtobufCMessageDescriptor nspanel_status_report__descriptor; extern const ProtobufCEnumDescriptor nspanel_status_report__state__descriptor; diff --git a/protobuf_firmware/protobuf_nspanel_entity.pb-c.c b/protobuf_firmware/protobuf_nspanel_entity.pb-c.c index 4776cf1e..6b24a081 100644 --- a/protobuf_firmware/protobuf_nspanel_entity.pb-c.c +++ b/protobuf_firmware/protobuf_nspanel_entity.pb-c.c @@ -547,7 +547,7 @@ const ProtobufCEnumDescriptor nspanel_entity_state__media_player__playback_state nspanel_entity_state__media_player__playback_state__value_ranges, NULL,NULL,NULL,NULL /* reserved[1234] */ }; -static const ProtobufCFieldDescriptor nspanel_entity_state__media_player__field_descriptors[16] = +static const ProtobufCFieldDescriptor nspanel_entity_state__media_player__field_descriptors[17] = { { "media_player_id", @@ -741,6 +741,18 @@ static const ProtobufCFieldDescriptor nspanel_entity_state__media_player__field_ 0, /* flags */ 0,NULL,NULL /* reserved1,reserved2, etc */ }, + { + "volume_step", + 17, + PROTOBUF_C_LABEL_NONE, + PROTOBUF_C_TYPE_INT32, + 0, /* quantifier_offset */ + offsetof(NSPanelEntityState__MediaPlayer, volume_step), + NULL, + NULL, + 0, /* flags */ + 0,NULL,NULL /* reserved1,reserved2, etc */ + }, }; static const unsigned nspanel_entity_state__media_player__field_indices_by_name[] = { 15, /* field[15] = album_art_url */ @@ -759,11 +771,12 @@ static const unsigned nspanel_entity_state__media_player__field_indices_by_name[ 8, /* field[8] = source_volume */ 2, /* field[2] = state */ 5, /* field[5] = volume */ + 16, /* field[16] = volume_step */ }; static const ProtobufCIntRange nspanel_entity_state__media_player__number_ranges[1 + 1] = { { 1, 0 }, - { 0, 16 } + { 0, 17 } }; const ProtobufCMessageDescriptor nspanel_entity_state__media_player__descriptor = { @@ -773,7 +786,7 @@ const ProtobufCMessageDescriptor nspanel_entity_state__media_player__descriptor "NSPanelEntityState__MediaPlayer", "", sizeof(NSPanelEntityState__MediaPlayer), - 16, + 17, nspanel_entity_state__media_player__field_descriptors, nspanel_entity_state__media_player__field_indices_by_name, 1, nspanel_entity_state__media_player__number_ranges, diff --git a/protobuf_firmware/protobuf_nspanel_entity.pb-c.h b/protobuf_firmware/protobuf_nspanel_entity.pb-c.h index 4d2d1925..89eb5e34 100644 --- a/protobuf_firmware/protobuf_nspanel_entity.pb-c.h +++ b/protobuf_firmware/protobuf_nspanel_entity.pb-c.h @@ -138,10 +138,14 @@ struct NSPanelEntityState__MediaPlayer * the panel can compare it with the last URL to tell when to fetch again. */ char *album_art_url; + /* + * How much one volume up/down press changes the volume and source volume, in 1-100%. + */ + int32_t volume_step; }; #define NSPANEL_ENTITY_STATE__MEDIA_PLAYER__INIT \ { PROTOBUF_C_MESSAGE_INIT (&nspanel_entity_state__media_player__descriptor) \ - , 0, (char *)protobuf_c_empty_string, NSPANEL_ENTITY_STATE__MEDIA_PLAYER__PLAYBACK_STATE__UNKNOWN, (char *)protobuf_c_empty_string, (char *)protobuf_c_empty_string, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (char *)protobuf_c_empty_string } + , 0, (char *)protobuf_c_empty_string, NSPANEL_ENTITY_STATE__MEDIA_PLAYER__PLAYBACK_STATE__UNKNOWN, (char *)protobuf_c_empty_string, (char *)protobuf_c_empty_string, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (char *)protobuf_c_empty_string, 0 } typedef enum {