Smart home: HomeKit, Matter and Google Home under com.codename1.home - #5554
Smart home: HomeKit, Matter and Google Home under com.codename1.home#5554shai-almog wants to merge 93 commits into
Conversation
The framework has no smart-home API, so an app that wants to talk to a light, a lock or a thermostat has to drop into native code per platform. This is the portable half of fixing that: the model, the facade, the bridge interface each port will implement, and the wire format between them. No native code yet, and nothing in the builders, so this changes no behaviour on any platform -- the base CodenameOneImplementation returns no bridge and SmartHome answers NOT_SUPPORTED everywhere. The shape follows com.codename1.health: one entry point that never returns null, an inert fallback rather than a null check at every call site, and a flat primitives-and-strings SPI so an Objective-C port never constructs a Java object. Three decisions worth defending. The capability vocabulary is canonical rather than pass-through. Trait. BRIGHTNESS is one constant whether the accessory is behind HomeKit or Matter, and the port maps it. The alternative -- exposing HMCharacteristicType strings and Matter cluster ids -- would have meant every app writing platform branches inside a cross-platform API. The cost is that each mapping had to be decided, and the interesting ones are documented on the constant rather than buried: covering position runs the opposite way on the two backends, Matter's battery percentage is in halves, Matter has no single thermostat setpoint, and OUTLET_IN_USE has no Matter equivalent at all. Where a mapping is lossy -- air quality has six levels on one side and seven on the other -- the canonical answer keeps an escape hatch to the platform's own ordinal, because otherwise every judgment call becomes a permanent lie. Values are a tagged union, not a class per trait. A class per trait means a cast at every read site, and a failed cast does not throw under ParparVM. One TraitValue with kind-checked getters turns the same mistake into an IllegalStateException on every platform. It also keeps HealthQuantity's best idea: there is no zero-argument getDouble, so a Celsius setpoint cannot be read as Fahrenheit by accident. Mireds and Kelvin are reciprocal rather than affine, so Kelvin is deliberately not a TraitUnit and the conversion is a named method instead of a table entry that would be quietly wrong. Commissioning lives in its own package. The build server decides what native machinery an app gets by scanning bytecode for package prefixes and has no way to express an exclusion, so the package boundary has to be the permission boundary. Commissioning costs an entire generated Xcode target on iOS and the Play services dependency plus Bluetooth permissions on Android; an app that only reads its lights should get none of that. EdtResult and OneShot moved from com.codename1.impl.health to a new com.codename1.impl.async and are shared rather than copied. Their javadoc records three concurrency bugs a copy would have silently re-acquired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Almost all of a smart-home feature is code that has nothing to do with accessories -- laying out a room, wiring a switch to a write, handling a failure, deciding what to show while a covering is moving. A desktop port that answered NOT_SUPPORTED would make all of that testable only on a phone with real hardware attached, which in practice means testable rarely. So the simulator, the desktop ports and the JavaScript port get a real implementation rather than a stub: LocalHomeBridge, an app-private home that reads, writes, runs scenes and commissions. It reports LOCAL_ONLY, not AVAILABLE, because nothing outside the app can see these accessories. Two rules it follows that a mock would not, both of them about not letting an app be written against behaviour it cannot rely on: It never completes inline, and adds a few milliseconds of latency on purpose. Code written against a store that answers synchronously races the moment it meets one that does not, and that asymmetry has already shipped once here -- it is why EdtResult exists. It does not push changes. isPushDelivery() answers false and changes wait for drainChanges(), matching every backend except HomeKit in the foreground. A simulator that pushed would leave the polling path -- the one that actually runs on Android -- exercised nowhere. The synthetic house is deliberately awkward rather than tidy. It has a two-gang switch (one accessory, two services, both exposing ON_OFF, so "write to the accessory" is ambiguous and finds out here rather than in someone's hallway), a bridged pair of lights behind a hub, an unreachable socket that fails every operation, a thermostat in auto mode where TARGET_TEMPERATURE genuinely has no value, a sensor that has never reported, and a dimmer with a real 10 percent floor so a slider built from the trait's nominal range offers values the accessory refuses. Every availability state is scriptable, which is the only way most people will ever reach their app's COMMISSIONING_ONLY branch. 93 tests over the value layer, the wire codec, the Matter setup-payload parser (checked against the specification's own worked example) and the whole stack against the simulated home. The degradation suite is the one that earns its keep: it pins that a port with no backend answers empty rather than null and fails fast rather than hanging, which is the contract that lets application code drop the platform branch. One behaviour change fell out of writing them: reading or writing an empty batch now succeeds with nothing before the bridge is consulted rather than after. A request that asks for nothing has no platform component, and a caller whose filter emptied a list should not get a different answer on the desktop than on a phone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each returns the local simulated house rather than nothing, so a smart-home screen can be built and exercised on a laptop. JavaSEPort also exposes it statically, which is what the simulator's Smart Home panel will drive to script an accessory changing, an accessory going offline, or the availability reporting something other than LOCAL_ONLY -- the last of which is the only practical way to reach an app's COMMISSIONING_ONLY branch. The getters are class-guarded for the reason the health ones are: the bridge owns the graph, the current trait values and the undelivered-change queues, so two of them coordinate on nothing and a write through one would be invisible to a subscription registered against the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the SPI against HMHomeManager: the accessory graph, characteristic reads and writes, live notifications, action sets, and the MatterSupport add-device flow. Compiled only when the builder flips CN1_INCLUDE_HOMEKIT, and the #else branch supplies a trampoline for every native so a home-free app still links and carries no HomeKit symbols. Verified by type-checking the whole file against the iOS 26.2 SDK, in both the HomeKit-on and HomeKit-off configurations and with and without Matter setup: zero warnings under -Wall. That caught four things that would otherwise have failed a cloud build: three HMErrorCode constants that do not exist under the names I used, and a vestigial HMCharacteristicProperties local for a type HomeKit does not have. Three decisions worth recording. The graph is an encoded snapshot rebuilt on the main queue, not a live view. HMHomeManager must be created on the main thread and Apple documents no thread safety for HMHome, HMAccessory or HMCharacteristic -- but the SPI's graph getters are synchronous and are called from the EDT, and on iOS the EDT IS the main thread, so hopping and blocking would deadlock outright. Keeping encoded strings behind a lock means the getters never see a HomeKit object. It also happens to be what com.codename1.home already assumes. primaryHome is gone. Apple deprecated it in iOS 16.1 as "no longer supported" with no replacement, so isPrimary() now reports false for every home on iOS and getPrimaryStructure() falls back to the first. Guessing would have been a claim about what the user prefers. Commissioning goes through Swift, and both directions are looked up by name. MatterSupport has no Objective-C interface -- MatterAddDeviceRequest is a Swift struct with an async perform() -- so the flow lives in a Swift file. Bridging it the normal way needs a bridging header and a generated <Module>-Swift.h, both named after the application's module, which does not exist when a file inside the port is written. So each side finds the other with NSClassFromString and passes one dictionary. The Swift half type-checks clean against the SDK too. One bug fixed before it shipped: the batch read counted how many live reads to wait for in a pass that had not yet applied the error cases, so a batch containing an unreachable accessory counted a read it never issued and the caller waited forever. The two passes are now split so the count is fixed by the same logic that decides what to skip. Known gap, documented on the method: below iOS 17.6 a setup code the app already scanned is not passed through, because the only API those releases have was dropped from the Swift interface when Apple deprecated it. Apple's sheet asks the user to scan again and the accessory still commissions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…idge The port cannot call Google's smart-home libraries: it compiles against a fixed, old android.jar with no Play services, no AndroidX, no Kotlin and no coroutines. It does not need to -- the port ships to app builds as source that the app's own Gradle compiles -- so this follows the Health Connect and Android Auto pattern exactly: a SmartHomeDelegate interface speaking only primitives and Strings, a registry the injected implementation publishes itself through, and a bridge that forwards and decodes nothing. The injected half is plain Java rather than Kotlin, because the Play services Matter commissioning API is plain Java. That drops the Kotlin plugin, the stdlib and coroutines from what an app has to carry to add an accessory. It is verified rather than written blind: it compiles against the real play-services-home 16.0.0-beta1 AAR pulled from Google's Maven, against the real android.jar, and against the port itself. Android reports COMMISSIONING_ONLY, and that is the substance of the change rather than a placeholder. Play services can add a Matter accessory to the user's Google Home with no setup at all -- no account linking, no Cloud project, no console registration. What it cannot do is let the app then SEE or CONTROL that accessory: the graph belongs to Google Home, and reading it needs the Google Home APIs plus a Google Cloud project and a Home Developer Console registration carrying the developer's own signing-key SHA-1. None of that can be shipped on their behalf. So the graph is empty here and the availability says so, rather than reporting AVAILABLE and rendering an empty house with no explanation. getConfigurationProblems() spells out why, in text aimed at the developer. What is NOT in this change, stated plainly: the Google Home APIs graph. Its artifacts (play-services-home 17.x and play-services-home-types) are not publicly resolvable from Google's Maven -- I checked, they 404 -- which is consistent with the SDK being gated behind Developer Console access. Writing a Kotlin bridge against an API surface I cannot inspect or compile would have been guesswork dressed as a feature. Commissioning works today; the graph needs the SDK in hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The framework half is only half the feature. This is the other half: an app that never references com.codename1.home gets no HomeKit framework, no entitlement, no Play services dependency, no manifest change and no extra Xcode target -- and one that does gets exactly the pieces it asked for. The gating runs off the existing bytecode scanner in both builders, plus a PlatformFeatureCatalog entry pair. Three things fall out of it that are worth recording. The HomeKit entitlement is gated on touching accessories, NOT on using the package. This is the trap the HealthKit block already documents at length: com.apple.developer.homekit has to be granted on the App ID, so handing it to an app that merely rendered "smart home is unavailable on this device" would fail its codesigning for a capability it never wanted -- and the failure surfaces as an opaque codesign error minutes into a cloud build. So a scan that sees only SmartHome.getAvailability() and the capability enums links HomeKit and stops there. The classifier lives in a shared, tested helper rather than an inline list, and a parity test asserts the injection is guarded by the narrow flag rather than the broad one. Commissioning is separated because it is expensive. On iOS it costs MatterSupport, a second entitlement, an app group, a background mode, Bonjour service declarations and an entire generated app-extension target -- Apple requires a MatterAddDeviceExtension before an app may add an accessory, and an app without one gets a runtime failure with nothing at build time to warn it. Generating and signing that is the builder's job, because the point of the framework is that nobody opens Xcode. Since the scanner matches on a package prefix and cannot express an exclusion, the package boundary IS the permission boundary, which is why com.codename1.home.commissioning is its own package. There are deliberately no Android permissions. The obvious expectation is Bluetooth and local-network, because commissioning involves both. It does not need them: Play services runs the entire add-device interaction in its own activity holding its own permissions, and the play-services-home AAR declares no permissions at all -- which I checked rather than assumed. Adding them "to be safe" would put three Android 12 Bluetooth prompts in front of the users of an app that never scans. What IS needed is the <queries> entry, without which package-visibility filtering makes openEcosystemApp() silently do nothing. An existing guard earned its keep: WatchNativeBuilderTest failed because both new frameworks were classified for neither watchOS availability list. HomeKit is present on watchOS and the watch slice uses it; MatterSupport is present and the watch slice compiles the flow out, since Apple's sheet is iOS-only. Both now say which and why. 27 new tests over the fragments, the generated extension and scanner parity; the full 618-test plugin suite is green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows External-Surfaces rather than a feature tour: it opens with the model, then spends its length on the four things that will otherwise bite somebody. Android's default answer is not "available" gets its own section, because it is the one thing worth understanding before designing a screen. An app written against the iOS meaning of AVAILABLE renders an empty house on Android with no explanation. A reading has three outcomes, not two. The example branches on all of them, because "no value yet" is the one people skip and it renders as zero degrees. Nothing wakes your app, so isPushDelivery() has to be asked and drainChanges() has to be wired to the foreground -- an app that assumes push looks exactly like a sensor that never fires. Commissioning success does not always mean you got a device. The build-hint section says what is automatic in both directions: an app that never references the package carries none of it, and the HomeKit entitlement arrives only for an app that touches accessories, which is why an availability-only app does not need the App ID capability. Five snippets, all compiled as part of docs/demos rather than pasted, so an API change breaks them. Vale is clean and the paragraph-capitalization gate passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine anonymous SmartHomeDelegate.Callback classes became named static ones. Anonymous callbacks capture the enclosing bridge and pin it for as long as the platform holds them, which is what SIC_INNER_SHOULD_BE_STATIC_ANON reports -- and the names read better in a stack trace than AndroidHomeBridge$7. AndroidSmartHomeSupport gets a spotbugs-exclude entry alongside the two identical registries already there, AndroidCarSupport and AndroidHealthSupport. Storing the injected delegate in a static field is the whole point: the port compiles against an android.jar with no Play services on it, so it cannot hold the bridge any other way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The edit that added getHomeBridge() went through a script that rewrote the whole file, and AndroidImplementation.java is CRLF with 26 mixed lone-LF lines. Normalizing it turned a 22-line addition into a 14,260-line diff that would have buried the change in review and conflicted with every other branch touching the file. Restored from origin/master and re-applied byte-wise. The diff is now the 22 lines it should always have been. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
|
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Cloudflare Preview
|
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 149 screenshots: 149 matched. |
|
Compared 149 screenshots: 149 matched. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3cc23a11b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 149 screenshots: 149 matched. Benchmark ResultsDetailed Performance Metrics
|
|
Compared 181 screenshots: 181 matched. |
PMD and Checkstyle fixes across the new packages, plus four defects the review found: - HomeKit authorization answered from the delegate. Creating HMHomeManager is what prompts, and the prompt is asynchronous, so reading authorizationStatus straight afterwards reported NOT_DETERMINED with the sheet still on screen. requestAuthorization() now waits for didUpdateAuthorizationStatus:/homeManagerDidUpdateHomes: and answers with the user's actual choice; a second request while one is open is answered UNKNOWN rather than left hanging. - Per-write credentials. writeTraits carried one authorization string for the whole batch, filled by whichever write had one last, so a batch with two locks sent the second lock's PIN to the first. It is now a positionally aligned array like every other field. - A stopped subscription delivers nothing. The initial-values branch dispatched without checking disposed, and the EDT hop did not re-check it when it ran, so a torn-down form could still be handed a batch. - Commissioner's unsupported path returned a plain already-failed AsyncResource, which calls back inline on whichever thread attached the listener. It is an EdtResult now, like the rest of the API. The catalog no longer names iOS frameworks for smart home or for the broad health entry. Entries match on a prefix and their frameworks are linked for real, so the health entry linked HealthKit into sensor-only apps -- which Apple rejects without health purpose strings -- and the home entries were a second copy of a decision IPhoneBuilder makes under gates the table cannot express, including the ios.home.commissioning opt-out. The MatterSupport Swift shim moves out of the port's nativeSources into the builder resources, injected only for a commissioning build. Everything in nativeSources is unpacked into every iOS build, and an unused .swift does not compile away the way an #ifdef'd .m does -- its presence alone flips the project into Swift mode, and it reached the watch and tv slices where MatterAddDeviceRequest does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c98e011b23
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
The rendered guide turned the `Type`+s+ plural idiom into a two-letter token LanguageTool could not spell, four times over; the sentence names the types in parentheses instead. Plus a noun-form 'a write', two ranges written with the .. operator, and one British spelling. 'mired' and 'setpoint' are the terms the platforms and the HVAC industry use, so they join the accept list rather than the prose being reworded around them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f781f094e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Microsoft.Contractions is an error-severity rule in this guide, and 'are *not* supported' matched it. The emphasis moves onto the whole clause rather than being dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5631f0c828
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
iOS builds: - needsXcodeProjectMutation now includes matterExtensionEnabled. Without it, a commissioning app with no pods and no other extension skipped the whole xcodeproj mutation block, so the CN1MatterSetup target was never generated or embedded -- the ordinary commissioning-only build. - The Swift shim is staged from appendMatterExtensionTarget, while the schemes ruby is still being assembled. Copied after that script has run it was on disk and in no target, so NSClassFromString found nothing and every commissioning call failed as NOT_CONFIGURED. Android commissioning: - restoreIntentResultListener() on every path out of the flow. The activity latches waitingForResult when a listener registers and ignores every later registration until that clears, so skipping it did not merely leak this listener -- it broke the camera, the scanner and the file picker for the rest of the process. Framework: - A written value is converted to the trait's own unit once, here, rather than in each bridge. TraitValue.of(68, FAHRENHEIT) for a Celsius trait reached HomeKit's write path, which reads the number and ignores the unit, as 68 degrees Celsius. Scene actions get the same treatment, being writes that happen later. - CommissioningRequest.setTimeoutMillis is honoured. Neither platform flow takes a timeout, so a caller who asked for one waited forever on a sheet that could not come back. - refresh() during startup is deferred and answered with the start's own result. Setting started before start() had answered sent a second caller to the bridge's refresh(), which on iOS rebuilds a snapshot of an HMHomeManager that has not loaded and resolves with no homes. - SetupPayload validates each base-38 group against the bytes it stands for and each manual-code chunk against its bit width. A five-character group holds more than three bytes and a five-digit chunk more than 16 bits, so malformed codes aliased valid ones -- accepted here and rejected by the platform, which is the failure this parser exists to prevent. iOS scenes are all-or-nothing: an action that cannot be built or added now fails the request and the half-built action set is removed. A "Good night" that silently drops the lock is worse than one that failed, because the user is told it exists and runs it every night. The retained arguments are also released on the two early exits that leaked them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running, creating or deleting a scene went straight to the bridge, so a scene an app kept -- in its own storage, or across a stop() -- named a home the native cannot resolve until the HomeKit database has loaded, and every cold launch answered INVALID_ARGUMENT for a scene that was there. All three go through the same start path reads, writes, subscriptions and identify use. And the vendor id is emitted as the number it parsed to. "0XFFF1" parses happily in Java and is not a Swift integer literal at all -- Swift wants a lowercase x -- so echoing back what was written produced an extension that did not compile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb6d41fda5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A write batch binds a single thermostat setpoint to the caller's own mode write, and it bound it to the first entry that was not AUTO. "HEAT, then AUTO, and a target" therefore applied the target behind the HEAT write while the AUTO write left the thermostat in AUTO, holding a target nothing can read back -- the outcome the refusal exists to prevent. It binds to what the batch actually asks for now, and refuses outright when the batch names two different modes for one thermostat: HomeKit runs those writes independently, so which one the accessory ends in is whichever landed last, and no answer here would be honest. On the builder side, injectedPlistString answers null both for a privacy key ios.plistInject does not carry and for one it gives <false/>. The two need opposite handling -- an absent key takes the generated default, a present one suppresses it in the renderer -- so <false/> passed validation as absent-but-defaultable and shipped a commissioning app whose Bluetooth purpose string was the boolean false. injectedPlistValueTag reports the kind of value the fragment gives a key, and anything that is not a nonblank string is refused with the rest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1c37bffdc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…uest Two builder checks looked for a key by searching the ios.plistInject fragment for its name. A comment that mentions NSBonjourServices -- or an unrelated string that contains it -- was therefore a declaration: the Bonjour check read the array of whatever key came after the comment and refused a build whose plist listed both Matter services correctly, and the query-schemes check skipped its own hint, so the plist shipped with no com.apple.Home entry at all and openEcosystemApp reported Apple Home missing on a device that has it. Both now find the real key element and read the array that belongs to it, through one comment- and CDATA-aware reader shared with the purpose-string validation. On Android, a commissioning request whose limit expires is failed in the framework, which knows nothing about the delegate's parked callback. When Play services answered neither of its listeners -- no IntentSender, no failure -- nothing cleared it, and every later commission() was refused with BUSY for the life of the process with no screen in front of the user. An expired request that never launched is reclaimed now. A launched one is not: the sheet is the user's to finish, and its answer arrives through the activity result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4530b33f64
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…6.4 extension miredToKelvin and kelvinToMired guarded with `<= 0`, and NaN compares false against every operator -- so it went straight through a check whose contract says the input must be positive, and the reciprocal of NaN is NaN. One bad reading then spread through every calculation it touched instead of failing at the conversion. Named on its own now, with a test for each direction. An own-fabric commissioning build compiles its extension for iOS 16.4, because Apple's Matter framework starts there, while the app itself keeps the 16.1 floor MatterSupport asks for -- raising the whole app would cost every user on 16.1 through 16.3 the application over a feature they never asked for. Those three releases cannot load the extension, and the host offered commissioning anyway: the sheet opened onto nothing. Commissioner.getStyle() answers ECOSYSTEM_APP_HANDOFF there and the call itself is refused with a message, so the app hands off to the Home app exactly as a build with no extension does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3c779b8776
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
HomeKit has no request-and-callback API -- creating the manager is what prompts, and it prompts once. A second requestAuthorization() while that sheet was on screen was answered UNKNOWN on the spot, so an app that asks at startup and again from its first screen, which is the ordinary shape, showed one of them a refusal moments before the user granted access, and that request was never corrected. All of them wait now, and all of them get the decision. The plist-fragment reader also answers with the key's OWN value. Scanning forward for the next <string> anywhere after the key, a key given <false/> answered with an unrelated later key's string -- so the HomeKit purpose-string check passed on a value the renderer keeps as the boolean false, and the app was terminated on the device for a disclosure the build had just approved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e6ffcb9886
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The single-setpoint refusal only ran when the thermostat was ALREADY in AUTO, so a thermostat in HEAT given "AUTO, and 21" passed both writes: the mode landed, the target was reported as applied, and an immediate read answered absent because a thermostat in AUTO has no single target. The local backend refused the same batch, because it applies mode writes first and then asks what the thermostat is. The check runs from any starting state now and asks what the batch LEAVES it in -- and a target that waits on a mode write says so when that write fails, whatever mode it was going to. The Info.plist renderer also asks what the injected fragment DECLARES rather than what its text contains. A comment mentioning NSBluetoothAlwaysUsageDescription suppressed the generated default, so a commissioning build shipped with no Bluetooth purpose string and iOS killed it the moment the flow touched Bluetooth -- while the validation, which parses the fragment properly, had just approved the default it was about to drop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6be43dd64
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The wire carries an ordinal and nothing else, so a decoded enum value arrived with no constant name while the value an app builds with ofEnum carries one -- and the two were unequal. Comparing a reading against a desired state is the most ordinary thing anyone does with one, and it was false every time: a lock that reported SECURED did not equal ofEnum(LockState.SECURED), and a map keyed on values missed. The trait knows what its ordinals are called, so the codec names the value once the ordinal is known to be in the trait's domain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 20f3280bcd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A scripted TARGET_TEMPERATURE on a thermostat in AUTO went out to subscribers as a value, while a read of the same trait answers absent -- there are two thresholds in AUTO and no single target, which is what the device says too. A UI drove itself off the change and showed a number the very next read denied the existence of. The value is still stored, because leaving AUTO brings it back; the change is announced as absent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b12666cc54
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Every iOS LOCK_STATE and AIR_QUALITY reading carries the backend's own ordinal alongside the canonical value, and no value an app builds ever does -- so equals() comparing that metadata kept a reading unequal to the value it plainly is, even once the enum name was there. It is what getRawPlatformValue() exists to expose, deliberately: two equal values can have come from different platform states, and an app that cares reads them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51d4f3569d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
SetupPayload uppercased the QR payload with String.toUpperCase(), which follows the device's locale: in Turkish a lowercase i becomes a dotted capital that is not in the base-38 alphabet, so a code that scans everywhere else was reported as malformed in one language, on the phone the user actually owns. The alphabet is ASCII by specification, so the normalization is now ASCII too -- toUpperCase(Locale) is not in the profile this code targets. The iOS builder also validates the purpose string the RENDERER will use. ios.plistInject wins there -- a generated value is emitted only for a key the fragment does not declare -- so an app with a perfectly good ios.NSHomeKitUsageDescription beside a fragment carrying that key as <false/> passed the build and shipped the false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 51ebffa288
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ile gate commission() and stop() arrive on the Codename One EDT while the Play services listeners and the activity result arrive on Android's main thread, and the state between them -- the parked callback, its generation, its deadline, whether a screen went up -- was neither volatile nor locked. The two threads had no ordering at all: a task finishing as a new request started could read a stale generation and answer, or clear, the callback that had just been parked for somebody else. The check-then-claim in commission() was a race of its own, where two callers could both find the slot free. One lock covers every access now, the generation check and the hand-off happen in a single step, and no callback is invoked while it is held. The injected bridge also gets a compile gate. It is a .javas resource copied into a generated Android project, so nothing in this repository compiled it: a broken edit shipped and failed in a customer's Gradle build, naming a file they never wrote. It now compiles in the plugin's own tests against the port's real SmartHomeDelegate and minimal stubs for the Android and Play services types. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bb1cab0ab
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…unch CodenameOne_GLViewController.h undefines CN1_INCLUDE_MATTER_SETUP on watchOS, tvOS, Catalyst and macOS, and its own comment says getStyle() reports NONE there. It reported ECOSYSTEM_APP_HANDOFF, which is the answer for an iOS build that merely left the extension out -- so an app took the advertised handoff and offered an Apple Home fallback these slices cannot perform. The two cases are distinguished now. On Android the sheet is launched under the same lock that decides it may be. Decided and then acted on, a stop() landing in between still opened the sheet: the result channel was taken for a request that no longer existed, so whatever started next was refused BUSY by a screen nobody was waiting on, and the sheet's own answer was discarded when it came back. The result listener moved into a method of its own so the body is not four levels deep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
check-copyright-headers refused the twenty stub declarations the injected Matter bridge is compiled against, and they should not carry that header: each mimics the shape of somebody else's published API -- a signature and a return of the right type -- and stamping our licence on a facsimile of android.app.Activity is a claim we have no business making. They are .javas now, like the bridge itself, which is the extension this repository already uses for Java that nothing here compiles; the test copies them under their real names into its own temporary tree. A README beside them says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2de0f5adc6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
FanMode.OFF means "not running", and HomeKit keeps that in the power characteristic: TargetFanState stays manual or auto while the fan stands still. A read of the mode therefore answered ON or AUTO for a fan that was not running -- the one state OFF is defined as was unreachable -- and a subscription to FAN_MODE watched only TargetFanState, so switching the fan off was a mode change no listener ever heard. The mode is derived from the power characteristic now, and the power characteristic is registered as the subscription's dependency, exactly as a thermostat's mode already is for its setpoint. Both pairs go through one map: a governing characteristic whose movement is delivered as the trait it governs, on the push path and on the polling recovery path alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc55317b61
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Creating HomeKit's manager is what prompts, so a process that has not created one cannot tell a user who authorized months ago from one who refused from one with no home set up -- and getAvailability() must not put a permission sheet on screen to find out. It answered PERMISSION_REQUIRED for all of them on every cold launch, so an app following the documented branch called requestAuthorization(), which iOS answers once and never again: no sheet, no graph, and the quick start in SmartHome's own javadoc returned having done nothing. NOT_STARTED says what is true, and the example now refreshes first and branches on what the connection found. The iOS header's constants are checked against the enums that define them. Nothing crosses this wire as a name except errors -- an availability, an authorization status, a value kind, a structure-change kind and a commissioning style all travel as ordinals a human copied into CN1SmartHome.h -- so appending a constant silently repoints a define, and the failure is a device reporting the wrong state, which no build and no simulator run can show. The defines carry their Java names now so the comparison is exact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7361ae05e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
An accessory can come back with a characteristic that no longer advertises event notifications, and the rebind took a fresh polling baseline and then dropped the entry from the recovery set. The subscription had already been handed to the caller as push -- isPushDelivery() is a property of that handle and cannot be taken back -- so nobody was ever going to call drainChanges() for it: one resyncRequired went out, and after that the listener sat on the value it had for as long as the screen stayed open, with no pusher and no poller. It stays on the recovery path now, which polls it and keeps retrying the registration, so it heals if the accessory comes back advertising notifications. The retry asks only where notifications are advertised. A HomeKit that answered a pointless request without an error would have ended the polling and left the watch with nothing; and a characteristic that can neither notify nor be read is dropped instead of keeping the recovery timer alive for the life of the process to ask something that will never answer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a first-class smart-home API. An app can list the accessories in a user's
home, read and write what they do, watch them for change, run scenes, and add
new Matter accessories -- with one set of code across HomeKit, Matter and
Google Home.
Paired with BuildDaemon #TBD, which carries the cloud-builder half of the
injection.
What's here
com.codename1.home, shaped likecom.codename1.health: one entry point thatnever returns null, an inert fallback rather than a null check at every call
site, and a flat primitives-and-strings SPI so an Objective-C port never
constructs a Java object.
wire format.
commissioning bridge.
uses.
Decisions worth reviewing
The trait vocabulary is canonical, not pass-through.
Trait.BRIGHTNESSisone constant whether the accessory is behind HomeKit or Matter. The mapping is
where the bugs live, so it is documented on each constant rather than buried:
covering position runs the opposite way on the two backends, Matter's battery
percentage is in halves, Matter has no single thermostat setpoint, and
OUTLET_IN_USEhas no Matter equivalent at all. Where a mapping is lossy thecanonical answer keeps an escape hatch to the platform's own ordinal.
The HomeKit entitlement is gated on touching accessories, not on using the
package. It has to be granted on the App ID, so an app that merely rendered
"smart home is unavailable" would fail codesigning for a capability it never
wanted. An availability-only app links HomeKit and stops there.
Commissioning is its own package because on iOS it costs an entire
generated app-extension target. The scanner matches on a package prefix and
cannot express an exclusion, so the package boundary has to be the permission
boundary.
Android reports
COMMISSIONING_ONLYby default, and that is the feature.Play services can add a Matter accessory with no setup at all; reading or
controlling one needs the Google Home APIs plus a Google Cloud project and a
Home Developer Console registration only the developer can create. Reporting
AVAILABLEwould make the constant mean something different on Android than oniOS.
No Android permissions. Play services runs the whole add-device interaction
in its own activity and the AAR declares none -- checked, not assumed. Adding
Bluetooth "to be safe" would prompt the users of an app that never scans.
Verification
configurations with zero warnings under
-Wall. That caught threeHMErrorCodeconstants that do not exist under the names used and avestigial local for a type HomeKit does not have.
play-services-homeAAR from Google's Maven, the realandroid.jarand theport.
package-info, since-tags, copyright and cast-semantics all pass.
Not claimed, and said so in the API
Automations and triggers; background accessory events; topology writes;
cameras; alarm panels; Matter events (which is why
LockState.JAMMEDisunreachable outside HomeKit); and the Google Home APIs accessory graph on
Android -- its artifacts are not publicly resolvable from Google's Maven, so
writing that bridge would have been guesswork. Each has a query that answers
honestly rather than a silence.
🤖 Generated with Claude Code