Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,15 @@ jobs:
- name: Configure Pages
uses: actions/configure-pages@v5

- name: Prepare public installer artifact
run: |
mkdir -p pages-site
rsync -a --delete --exclude 'superpowers/' docs/ pages-site/

- name: Upload installer artifact
uses: actions/upload-pages-artifact@v4
with:
path: docs
path: pages-site

- name: Deploy to GitHub Pages
id: deployment
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ cmake_minimum_required(VERSION 3.16)
set(MINIMAL_BUILD ON)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(PROJECT_VER "0.3.3")
set(PROJECT_VER "0.3.4")
project(ios_ancs_capture_c6)
26 changes: 13 additions & 13 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ The project bridges iOS notifications into local Home Assistant automations thro

Holding BOOT for 3 seconds opens the setup AP or iPhone enrollment recovery window. If a BLE bond already exists, enrollment actions request reconnect to the known iPhone only and do not allow a new phone to pair.

## Supported Boards And v0.3.3 Build Facts
## Supported Boards And v0.3.4 Build Facts

The shared firmware uses a 4 MB minimum flash layout. All v0.3.3 images shown in the installer are compile, link, partition, and merged-image verified. ESP32-S2 is excluded because it has no BLE. ESP32-H2 is excluded because it has no Wi-Fi, and ESP32-P4 has no integrated Wi-Fi/BLE radio.
The shared firmware uses a 4 MB minimum flash layout. All v0.3.4 images shown in the installer are compile, link, partition, and merged-image verified. ESP32-S2 is excluded because it has no BLE. ESP32-H2 is excluded because it has no Wi-Fi, and ESP32-P4 has no integrated Wi-Fi/BLE radio.

| Target | Typical module/board | Factory image | v0.3.3 status |
| Target | Typical module/board | Factory image | v0.3.4 status |
| --- | --- | ---: | --- |
| `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,425,616 bytes | Build verified; limited board flash/boot/AP proof |
| `esp32c2` | ESP32-C2 | 1,445,488 bytes | Build verified |
| `esp32c3` | ESP32-C3 | 1,634,528 bytes | Build verified |
| `esp32c5` | ESP32-C5 | 1,779,664 bytes | Build verified |
| `esp32c6` | ESP32-C6 | 1,779,680 bytes | Build verified; older hardware evidence is historical |
| `esp32c61` | ESP32-C61 | 1,722,800 bytes | Build verified |
| `esp32s3` | ESP32-S3 | 1,407,600 bytes | Build verified |
| `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,427,376 bytes | Build verified; limited board flash/boot/AP proof |
| `esp32c2` | ESP32-C2 | 1,447,504 bytes | Build verified |
| `esp32c3` | ESP32-C3 | 1,636,560 bytes | Build verified |
| `esp32c5` | ESP32-C5 | 1,781,696 bytes | Build verified |
| `esp32c6` | ESP32-C6 | 1,781,712 bytes | Build verified; older hardware evidence is historical |
| `esp32c61` | ESP32-C61 | 1,724,816 bytes | Build verified |
| `esp32s3` | ESP32-S3 | 1,409,392 bytes | Build verified |

The table describes the published v0.3.3 images. Build verification, physical flashing, BLE enrollment, and live iPhone notification capture are separate validation scopes; boards without physical-device evidence are labeled as build verified only.
The table describes the published v0.3.4 images. Build verification, physical flashing, BLE enrollment, and live iPhone notification capture are separate validation scopes; boards without physical-device evidence are labeled as build verified only.

## Home Assistant And HACS

Expand All @@ -62,7 +62,7 @@ Adding the HACS integration does not ask for an MQTT base topic. It uses the `La

The `Notification` event entity and detail entities are registered on a separate `iOS ANCS (...)` device, not on the MQTT device and not through a `via_device` relationship. The MQTT and iOS ANCS devices remain independent even when they represent the same physical relay. The integration privately saves the last complete notification so detail sensors survive a Home Assistant restart even when the source MQTT notification is not retained. The event entity still emits only newly received notifications and never replays the saved details as a new event. Reconfiguring a legacy manual-topic entry preserves its entity IDs and history while moving its companion entities to the separate iOS ANCS device.

The companion integration keeps MQTT Discovery entities unchanged and enabled. On the separate iOS ANCS device, it adds the notification event, 25 purpose-specific sensors and 11 strict binary sensors, plus a diagnostic `Raw notification` sensor. Text shown as a sensor state is limited to 255 characters; long values remain in the `full_value` attribute and the complete raw JSON remains in attributes. Notification titles and messages can be recorded by Home Assistant unless those entities are excluded from Recorder.
The companion integration keeps MQTT Discovery entities unchanged and enabled. On the separate iOS ANCS device, it adds the notification event, 24 purpose-specific sensors and 12 strict binary sensors, plus a diagnostic `Raw notification` sensor. Text shown as a sensor state is limited to 255 characters; long values remain in the `full_value` attribute and the complete raw JSON remains in attributes. Notification titles and messages can be recorded by Home Assistant unless those entities are excluded from Recorder.

Example automation:

Expand All @@ -72,7 +72,7 @@ homeassistant/automation_ios_ancs_c6_relay.yaml

Before enabling it, replace `sensor.replace_with_your_last_notification_entity` with your discovered last-notification sensor and `notify.replace_with_your_mobile_app_service` with your mobile app notify service. The example forwards only new `relay_id` state changes and filters `complete=false`, `pre_existing=true`, `unknown`, `unavailable`, and availability-restore transitions.

Notification JSON preserves the original `app_id` and adds a friendly `app_name`. Unknown bundle identifiers fall back to the original ID. Representative mappings are in [App ID Reference](docs/APP_ID_REFERENCE.md).
Notification JSON preserves the original `app_id` and adds a friendly `app_name`. The firmware prefers the localized Display Name supplied by the iPhone over ANCS. An app's first notification can require one extra ANCS lookup; later notifications in the same connection session use a bounded cache. If lookup fails, the complete notification is still delivered and falls back to the existing static mapping and then the original app ID. Representative fallback mappings are in [App ID Reference](docs/APP_ID_REFERENCE.md).

## Troubleshooting

Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,21 @@ BOOT 버튼을 3초 눌러 설정 AP 또는 iPhone 등록 창을 엽니다. 저

Home Assistant의 **iPhone 등록 시작** 버튼이나 BOOT 버튼을 3초 눌러 120초 등록 창을 열고, iOS Bluetooth 설정에서 PIN `123456`을 입력한 뒤 알림 공유를 허용합니다. 저장된 기존 iPhone 페어링 정보가 있으면 같은 동작은 기존 iPhone 재연결만 요청합니다.

## 지원 보드와 v0.3.3 빌드 사실
## 지원 보드와 v0.3.4 빌드 사실

공통 펌웨어는 최소 4 MB 플래시 레이아웃을 사용합니다. ESP32-S2는 BLE가 없고, ESP32-H2는 Wi-Fi가 없으며, ESP32-P4는 내장 Wi-Fi/BLE 라디오가 없어 제외됩니다.

| Target | 일반 모듈/보드 | Factory 이미지 | v0.3.3 상태 |
| Target | 일반 모듈/보드 | Factory 이미지 | v0.3.4 상태 |
| --- | --- | ---: | --- |
| `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,425,616 bytes | 빌드 검증, 제한적 실보드 플래시/부팅/AP 검증 |
| `esp32c2` | ESP32-C2 | 1,445,488 bytes | 빌드 검증 |
| `esp32c3` | ESP32-C3 | 1,634,528 bytes | 빌드 검증 |
| `esp32c5` | ESP32-C5 | 1,779,664 bytes | 빌드 검증 |
| `esp32c6` | ESP32-C6 | 1,779,680 bytes | 빌드 검증, 이전 하드웨어 증거는 과거 참고 |
| `esp32c61` | ESP32-C61 | 1,722,800 bytes | 빌드 검증 |
| `esp32s3` | ESP32-S3 | 1,407,600 bytes | 빌드 검증 |
| `esp32` | ESP32-WROOM-32 / WROOM-D32 | 1,427,376 bytes | 빌드 검증, 제한적 실보드 플래시/부팅/AP 검증 |
| `esp32c2` | ESP32-C2 | 1,447,504 bytes | 빌드 검증 |
| `esp32c3` | ESP32-C3 | 1,636,560 bytes | 빌드 검증 |
| `esp32c5` | ESP32-C5 | 1,781,696 bytes | 빌드 검증 |
| `esp32c6` | ESP32-C6 | 1,781,712 bytes | 빌드 검증, 이전 하드웨어 증거는 과거 참고 |
| `esp32c61` | ESP32-C61 | 1,724,816 bytes | 빌드 검증 |
| `esp32s3` | ESP32-S3 | 1,409,392 bytes | 빌드 검증 |

표의 상태는 공개된 v0.3.3 이미지 기준입니다. 빌드 검증, 실제 보드 플래시, BLE 등록, iPhone 알림 수신은 서로 다른 검증 범위이며 보드별 실기기 결과가 없는 항목은 빌드 검증으로만 표시합니다.
표의 상태는 공개된 v0.3.4 이미지 기준입니다. 빌드 검증, 실제 보드 플래시, BLE 등록, iPhone 알림 수신은 서로 다른 검증 범위이며 보드별 실기기 결과가 없는 항목은 빌드 검증으로만 표시합니다.

## Home Assistant와 HACS

Expand All @@ -70,7 +70,7 @@ HACS 통합을 추가할 때 MQTT 기본 토픽을 입력하지 않습니다.

`알림` 이벤트 엔티티와 상세 엔티티는 MQTT 장치와 분리된 별도 `iOS ANCS (...)` 장치에 등록됩니다. MQTT 장치를 `via_device`로 연결하지 않으며 같은 물리 장치를 나타내더라도 두 통합의 기기와 엔티티는 독립적으로 유지됩니다. 통합은 마지막 완전한 알림을 전용 저장소에 보관하므로 MQTT 알림이 retained가 아니어도 Home Assistant 재시작 뒤 상세 센서가 복원됩니다. 이벤트 엔티티는 새 알림만 표시하며 저장된 세부정보를 새 이벤트로 재생하지 않습니다. 기존 수동 토픽 항목은 통합의 **재구성**에서 MQTT 소스 장치를 선택하면 엔티티 ID와 기록을 보존한 채 별도 iOS ANCS 장치로 이동합니다.

동반 통합은 기존 MQTT Discovery 엔터티를 변경하거나 비활성화하지 않습니다. 별도 iOS ANCS 장치에 알림 이벤트, 목적별 센서 25개와 엄격한 바이너리 센서 11개, 진단용 `원본 알림` 센서를 추가합니다. 텍스트 센서 상태는 최대 255자로 제한되며 긴 값은 `full_value` 속성에, 전체 원본 JSON은 속성에 유지됩니다. 알림 제목과 내용 같은 개인정보는 Home Assistant Recorder에 기록될 수 있으므로 기록이 필요하지 않으면 해당 엔터티를 Recorder에서 제외하세요.
동반 통합은 기존 MQTT Discovery 엔터티를 변경하거나 비활성화하지 않습니다. 별도 iOS ANCS 장치에 알림 이벤트, 목적별 센서 24개와 엄격한 바이너리 센서 12개, 진단용 `원본 알림` 센서를 추가합니다. 텍스트 센서 상태는 최대 255자로 제한되며 긴 값은 `full_value` 속성에, 전체 원본 JSON은 속성에 유지됩니다. 알림 제목과 내용 같은 개인정보는 Home Assistant Recorder에 기록될 수 있으므로 기록이 필요하지 않으면 해당 엔터티를 Recorder에서 제외하세요.

MQTT Discovery의 compact model은 `최근 알림`, `알림 제목`, `알림 내용`, `앱 이름`, `장치 상태`, `장치 재시작` 엔티티를 중심으로 구성됩니다. 상태 속성에는 `ready`, `uptime_seconds`, `ble_connected`, `wifi_ssid` 같은 진단 값이 포함됩니다.

Expand All @@ -82,7 +82,7 @@ homeassistant/automation_ios_ancs_c6_relay.yaml

사용 전에 `sensor.replace_with_your_last_notification_entity`를 본인 Home Assistant의 last-notification sensor로, `notify.replace_with_your_mobile_app_service`를 본인 mobile app notify 서비스로 바꾸세요. 예제는 새 `relay_id` 상태 변화만 전달하고, `complete=false`, `pre_existing=true`, `unknown`, `unavailable`, availability 복구 전환은 전달하지 않습니다.

알림 JSON은 원본 `app_id`를 보존하고 표시용 `app_name`을 추가합니다. 목록에 없으면 원본 ID를 그대로 표시합니다. 대표 매핑은 [App ID Reference](docs/APP_ID_REFERENCE.md)에 있습니다.
알림 JSON은 원본 `app_id`를 보존하고 표시용 `app_name`을 추가합니다. 펌웨어는 iPhone이 ANCS로 제공한 현지화된 Display Name을 우선 사용합니다. 앱의 첫 알림은 이름 조회 때문에 한 번의 추가 ANCS 요청이 필요할 수 있고, 같은 연결 세션의 다음 알림부터는 제한된 세션 캐시를 사용합니다. 조회가 실패해도 알림 세부정보는 버리지 않고 기존 정적 매핑과 원본 앱 ID 순서로 폴백합니다. 대표 폴백 매핑은 [App ID Reference](docs/APP_ID_REFERENCE.md)에 있습니다.

## 문제 해결

Expand Down
5 changes: 5 additions & 0 deletions components/ancs_app_resolver/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
idf_component_register(
SRCS "ancs_app_resolver.c"
INCLUDE_DIRS "include"
REQUIRES ancs_protocol
)
8 changes: 8 additions & 0 deletions components/ancs_app_resolver/Kconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
menu "ANCS app name resolver"

config ANCS_APP_CACHE_CAPACITY
int "Session app display name cache entries"
default 16
range 1 64

endmenu
136 changes: 136 additions & 0 deletions components/ancs_app_resolver/ancs_app_resolver.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#include "ancs_app_resolver.h"

#include <string.h>

static void clear_output(char *output, size_t output_capacity)
{
if (output != NULL && output_capacity > 0U) {
output[0] = '\0';
}
}

static bool bounded_text(const char *value, size_t maximum, size_t *length)
{
if (value == NULL || length == NULL) {
return false;
}
*length = strnlen(value, maximum + 1U);
return *length > 0U && *length <= maximum;
}

static bool copy_output(const char *value,
char *output,
size_t output_capacity)
{
if (value == NULL || output == NULL || output_capacity == 0U) {
return false;
}
const size_t length = strlen(value);
if (length >= output_capacity) {
output[0] = '\0';
return false;
}
memcpy(output, value, length + 1U);
return true;
}

static ancs_app_resolver_entry_t *find_entry(ancs_app_resolver_t *resolver,
const char *app_id)
{
for (size_t index = 0; index < CONFIG_ANCS_APP_CACHE_CAPACITY; ++index) {
ancs_app_resolver_entry_t *entry = &resolver->entries[index];
if (entry->used && strcmp(entry->app_id, app_id) == 0) {
return entry;
}
}
return NULL;
}

static ancs_app_resolver_entry_t *replacement_entry(
ancs_app_resolver_t *resolver,
const char *app_id)
{
ancs_app_resolver_entry_t *target = find_entry(resolver, app_id);
if (target != NULL) {
return target;
}
for (size_t index = 0; index < CONFIG_ANCS_APP_CACHE_CAPACITY; ++index) {
if (!resolver->entries[index].used) {
return &resolver->entries[index];
}
}

target = &resolver->entries[0];
for (size_t index = 1; index < CONFIG_ANCS_APP_CACHE_CAPACITY; ++index) {
if (resolver->entries[index].age < target->age) {
target = &resolver->entries[index];
}
}
return target;
}

void ancs_app_resolver_init(ancs_app_resolver_t *resolver)
{
if (resolver != NULL) {
memset(resolver, 0, sizeof(*resolver));
}
}

ancs_app_resolution_t ancs_app_resolver_begin(
ancs_app_resolver_t *resolver,
const char *app_id,
char *output,
size_t output_capacity)
{
clear_output(output, output_capacity);
size_t app_id_length = 0U;
if (resolver == NULL || output == NULL || output_capacity == 0U ||
!bounded_text(app_id, CONFIG_ANCS_APP_ID_MAX, &app_id_length)) {
return ANCS_APP_RESOLUTION_USE_FALLBACK;
}
(void)app_id_length;

ancs_app_resolver_entry_t *entry = find_entry(resolver, app_id);
if (entry == NULL) {
return ANCS_APP_RESOLUTION_REQUEST_NATIVE;
}
if (!copy_output(entry->display_name, output, output_capacity)) {
return ANCS_APP_RESOLUTION_USE_FALLBACK;
}
entry->age = ++resolver->age;
return ANCS_APP_RESOLUTION_USE_NATIVE;
}

ancs_app_resolution_t ancs_app_resolver_complete(
ancs_app_resolver_t *resolver,
const char *app_id,
const char *display_name,
char *output,
size_t output_capacity)
{
clear_output(output, output_capacity);
size_t app_id_length = 0U;
size_t display_name_length = 0U;
if (resolver == NULL || output == NULL || output_capacity == 0U ||
!bounded_text(app_id, CONFIG_ANCS_APP_ID_MAX, &app_id_length) ||
!bounded_text(display_name,
CONFIG_ANCS_APP_NAME_MAX,
&display_name_length) ||
!copy_output(display_name, output, output_capacity)) {
return ANCS_APP_RESOLUTION_USE_FALLBACK;
}

ancs_app_resolver_entry_t *entry = replacement_entry(resolver, app_id);
entry->used = true;
entry->age = ++resolver->age;
memcpy(entry->app_id, app_id, app_id_length + 1U);
memcpy(entry->display_name, display_name, display_name_length + 1U);
return ANCS_APP_RESOLUTION_USE_NATIVE;
}

ancs_app_resolution_t ancs_app_resolver_fail(char *output,
size_t output_capacity)
{
clear_output(output, output_capacity);
return ANCS_APP_RESOLUTION_USE_FALLBACK;
}
44 changes: 44 additions & 0 deletions components/ancs_app_resolver/include/ancs_app_resolver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include "ancs_protocol.h"

#ifndef CONFIG_ANCS_APP_CACHE_CAPACITY
#define CONFIG_ANCS_APP_CACHE_CAPACITY 16
#endif

typedef enum {
ANCS_APP_RESOLUTION_REQUEST_NATIVE = 0,
ANCS_APP_RESOLUTION_USE_NATIVE,
ANCS_APP_RESOLUTION_USE_FALLBACK,
} ancs_app_resolution_t;

typedef struct {
bool used;
uint32_t age;
char app_id[CONFIG_ANCS_APP_ID_MAX + 1];
char display_name[CONFIG_ANCS_APP_NAME_MAX + 1];
} ancs_app_resolver_entry_t;

typedef struct {
uint32_t age;
ancs_app_resolver_entry_t entries[CONFIG_ANCS_APP_CACHE_CAPACITY];
} ancs_app_resolver_t;

void ancs_app_resolver_init(ancs_app_resolver_t *resolver);
ancs_app_resolution_t ancs_app_resolver_begin(
ancs_app_resolver_t *resolver,
const char *app_id,
char *output,
size_t output_capacity);
ancs_app_resolution_t ancs_app_resolver_complete(
ancs_app_resolver_t *resolver,
const char *app_id,
const char *display_name,
char *output,
size_t output_capacity);
ancs_app_resolution_t ancs_app_resolver_fail(char *output,
size_t output_capacity);
Loading
Loading