# Cozify Hub API Documentation

## Table of contents

- [Part 1 - Hub overview, authentication and authorization, and hub discovery](#part-1---hub-overview-authentication-and-authorization-and-hub-discovery)
- [1. General description of Hub](#1-general-description-of-hub)
- [2. Authentication and authorization](#2-authentication-and-authorization)
  - [2.1 Access model](#21-access-model)
  - [2.2 Factory-new and claimed lifecycle](#22-factory-new-and-claimed-lifecycle)
  - [2.3 Cloud authentication and logout](#23-cloud-authentication-and-logout)
  - [2.4 Getting HubKeys](#24-getting-hubkeys)
- [3. Hub discovery](#3-hub-discovery)
  - [3.1 Discovering the Hub on the local network](#31-discovering-the-hub-on-the-local-network)
  - [3.2 Reading Hub metadata](#32-reading-hub-metadata)
  - [3.3 Deriving the API version](#33-deriving-the-api-version)
  - [3.4 Recommended bootstrap sequence](#34-recommended-bootstrap-sequence)
  - [3.5 Local and remote access](#35-local-and-remote-access)
  - [3.6 HubKey content and Hub selection](#36-hubkey-content-and-hub-selection)
  - [3.7 User role and access rights](#37-user-role-and-access-rights)
- [Part 2 - Workflows, API calls, and shared API data structures](#part-2---workflows-api-calls-and-shared-api-data-structures)
- [4. Workflows](#4-workflows)
  - [4.1 Device pairing workflow](#41-device-pairing-workflow)
    - [4.1.1 Configuring devices with `CONFIGURABLE` capability](#411-configuring-devices-with-configurable-capability)
    - [4.1.2 Configuring IO-module ports during pairing and later management](#412-configuring-io-module-ports-during-pairing-and-later-management)
  - [4.2 Device workflow](#42-device-workflow)
  - [4.3 Group workflow](#43-group-workflow)
  - [4.4 Rule creation workflow](#44-rule-creation-workflow)
  - [4.5 Scene workflow](#45-scene-workflow)
  - [4.6 Room functionality](#46-room-functionality)
  - [4.7 Alarm functionality](#47-alarm-functionality)
- [5. API calls](#5-api-calls)
  - [5.1 General Hub endpoints](#51-general-hub-endpoints)
  - [5.2 Device endpoints](#52-device-endpoints)
  - [5.3 Group endpoints](#53-group-endpoints)
  - [5.4 Rule endpoints](#54-rule-endpoints)
  - [5.5 Scene endpoints](#55-scene-endpoints)
  - [5.6 Room endpoints](#56-room-endpoints)
  - [5.7 Alarm endpoints](#57-alarm-endpoints)
  - [5.8 BACnet endpoints](#58-bacnet-endpoints)
  - [5.9 Log endpoint](#59-log-endpoint)
- [6. API data structures](#6-api-data-structures)
  - [6.1 Hub feature codes](#61-hub-feature-codes)
  - [6.2 Common representation patterns](#62-common-representation-patterns)
  - [6.3 Hub metadata and lifecycle data](#63-hub-metadata-and-lifecycle-data)
  - [6.4 Poll and delta data](#64-poll-and-delta-data)
  - [6.5 User, role, and Hub-user data](#65-user-role-and-hub-user-data)
  - [6.6 Scan and pairing data](#66-scan-and-pairing-data)
  - [6.7 Device capabilities](#67-device-capabilities)
  - [6.8 Device data](#68-device-data)
  - [6.9 Group data](#69-group-data)
  - [6.10 Rule data](#610-rule-data)
  - [6.11 Scene data](#611-scene-data)
  - [6.12 Room data](#612-room-data)
  - [6.13 Alerts and alarms](#613-alerts-and-alarms)
  - [6.14 Zigbee network data](#614-zigbee-network-data)
- [Part 3 - Protocol operations and integration-specific chapters](#part-3---protocol-operations-and-integration-specific-chapters)
- [7. BACnet operations](#7-bacnet-operations)
  - [7.1 BACnet functionality](#71-bacnet-functionality)
  - [7.2 BACnet data](#72-bacnet-data)
- [8. Z-Wave operations](#8-z-wave-operations)
  - [8.1 Available Z-Wave commands](#81-available-z-wave-commands)
  - [8.2 Inclusion and exclusion](#82-inclusion-and-exclusion)
  - [8.3 Network maintenance and node management](#83-network-maintenance-and-node-management)
  - [8.4 Listing known Z-Wave nodes](#84-listing-known-z-wave-nodes)
  - [8.5 Reading and writing device configuration parameters](#85-reading-and-writing-device-configuration-parameters)
- [9. 433 devices](#9-433-devices)
  - [9.1 Manual pairing flow](#91-manual-pairing-flow)
  - [9.2 433 pairing commands and data](#92-433-pairing-commands-and-data)
  - [9.3 Virtual devices](#93-virtual-devices-created-from-a-device-description)
- [10. Modbus RTU](#10-modbus-rtu)
  - [10.1 Manual pairing flow](#101-manual-pairing-flow)
  - [10.2 Protocol configuration commands](#102-protocol-configuration-commands)
  - [10.3 Modbus device info](#103-modbus-device-info)
- [11. Modbus TCP server operations](#11-modbus-tcp-server-operations)
  - [11.1 Availability and network behavior](#111-availability-and-network-behavior)
  - [11.2 Register exposure model](#112-register-exposure-model)
  - [11.3 Supported Modbus function codes](#113-supported-modbus-function-codes)
  - [11.4 Modbus TCP server data](#114-modbus-tcp-server-data)
  - [11.5 Protocol configuration commands](#115-protocol-configuration-commands)
- [12. Autoconfiguration](#12-autoconfiguration)
  - [12.1 Autoconfiguration data](#121-autoconfiguration-data)
- [Part 4 - Cloud history and cloud video services](#part-4---cloud-history-and-cloud-video-services)
- [13. Cloud history service](#13-cloud-history-service)
  - [13.1 Base URL and authentication](#131-base-url-and-authentication)
  - [13.2 Device history values](#132-device-history-values)
  - [13.3 Typical history requests](#133-typical-history-requests)
  - [13.4 Chart period examples](#134-chart-period-examples)
  - [13.5 Hub event history](#135-hub-event-history)
- [14. Cloud video service](#14-cloud-video-service)
  - [14.1 Base URLs and authentication](#141-base-urls-and-authentication)
  - [14.2 Obtaining and refreshing a video token](#142-obtaining-and-refreshing-a-video-token)
  - [14.3 Video quota](#143-video-quota)
  - [14.4 Listing recordings](#144-listing-recordings)
  - [14.5 Playing, renaming, and deleting recordings](#145-playing-renaming-and-deleting-recordings)
  - [14.6 Live view and recording control](#146-live-view-and-recording-control)
- [Part 5 - Custom rules](#part-5---custom-rules)
- [15. Custom rules](#15-custom-rules)
  - [15.1 Availability and lifecycle](#151-availability-and-lifecycle)
  - [15.2 Device status and event messages](#152-device-status-and-event-messages)
  - [15.3 Rule base class and template types](#153-rule-base-class-and-template-types)
  - [15.4 Commands sent from rules](#154-commands-sent-from-rules)
  - [15.5 Rule source format](#155-rule-source-format)
  - [15.6 Commands and examples](#156-commands-and-examples)
  - [15.7 Custom-rule endpoints](#157-custom-rule-endpoints)
  - [15.8 Runtime behavior](#158-runtime-behavior)

Version tags used in this document:

- `1.14.15+` means the documented item is available in Hub version `1.14.15.*` and later.

Documented `1.14.15+` additions called out in this document:

- `GET /hub/shutdown`
- `POST /hub/remotepair`
- Device and state types `CELLULAR_MODEM` / `STATE_CELLULAR_MODEM`
- Device and state types `SPEAKER` / `STATE_SPEAKER`
- Device and state types `EMERGENCY` / `STATE_EMERGENCY`
- Device and state types `COZIFY_HUB` / `STATE_HUB`

## Part 1 - Hub overview, authentication and authorization, and hub discovery

## 1. General description of Hub

Cozify Hub is the local gateway and control point of a Cozify installation. It exposes the Hub API, maintains the current installation model, coordinates communication with connected devices and controllers, and executes automation-related operations. For API consumers, the Hub is the authoritative source for local state and the execution endpoint for Hub-scoped commands.

A client typically uses the Hub API in four ways: discover the Hub on the network, retrieve Hub metadata, read or synchronize the current state of the installation, and issue commands that change device or system behavior.

The Hub manages structured data about the installation, including devices, groups, scenes, rules, rooms, alarms, users, and Hub metadata. Some operations return complete objects, while others return deltas so that clients can synchronize incrementally instead of reloading the full state on every request.

Some responses contain localized human-facing text such as rule template labels, pairing help, and alert texts. The Hub localizes these fields before returning them. The language is selected from the request `Accept-Language` header, so clients should treat localized response fields as ordinary strings in the requested language rather than as translation dictionaries.

Operationally, the Hub has an important lifecycle distinction. A Hub starts in a `factory_new` state and moves to claimed once ownership has been established. Bootstrap operations are available during initial provisioning, and the normal authenticated API surface applies after the Hub has been claimed.

## 2. Authentication and authorization

Most Hub API operations require a HubKey in the Authorization header. A HubKey represents a user's access to a specific Cozify Hub and carries the role information used to authorize the request.

At a high level, access control has three layers: the caller authenticates to Cozify cloud services, obtains one or more HubKeys for the Hubs the caller can access, and then uses the HubKey associated with the selected Hub when calling the Hub API.

### 2.1 Access model

- Anonymous access is used for selected bootstrap operations such as reading Hub metadata, checking the effective access level of the current token, and claiming a factory-new Hub.
- Authenticated read access is used for operations that expose Hub state without granting administrative control.
- Administrative access is required for operations that modify protected configuration, pairing state, or other Hub-wide settings.

When authentication fails, the API uses standard HTTP semantics. `401 Unauthorized` indicates that the authentication information is missing, invalid, or expired. `403 Forbidden` indicates that the caller is authenticated but does not have sufficient rights for the requested operation.

### 2.2 Factory-new and claimed lifecycle

Lifecycle state affects which API paths are usable. A factory-new Hub exposes the bootstrap path needed to establish ownership. Once claimed, the Hub uses normal role-based access control for day-to-day API use.

The Hub metadata returned by `GET /hub` includes the Hub state, which allows clients to distinguish between an unclaimed device that requires provisioning and an operational device that is ready for authenticated API access.

The Hub lifecycle states relevant for API clients are:

- `factory_new`: the Hub has not yet been claimed and bootstrap operations are required.
- `claimed`: the Hub has been claimed and normal authenticated API access applies.

### 2.3 Cloud authentication and logout

Before a client can retrieve HubKeys, it must authenticate the user with Cozify cloud services. Cloud authentication creates the user session used for account-level operations such as listing available Hubs, retrieving HubKeys, and using the remote relay path.

The cloud authentication flow is:

1. Request a one-time password for the user's email address.
2. Receive the one-time password by email.
3. Send the email address and one-time password to the cloud login endpoint.
4. Receive a cloud token from the authentication response.
5. Store the cloud token in client-side persistent storage and use it in the Authorization header for subsequent cloud API calls such as HubKey retrieval or remote Hub access.

The email-based cloud authentication flow starts by requesting a one-time password to the user's email address.

Example request for email one-time password:

```text
POST https://api.cozify.fi/ui/0.2/user/requestlogin?email=developer@example.com
```

Shell example:

```bash
curl -X POST \
  'https://api.cozify.fi/ui/0.2/user/requestlogin?email=developer@example.com'
```

After the one-time password has been received by email, the client uses it to authenticate and obtain a cloud token.

Example cloud login request:

```http
POST https://api.cozify.fi/ui/0.2/user/emaillogin
Content-Type: application/json

{ "email": "developer@example.com", "password": "P7BQ8E" }
```

Shell example:

```bash
curl -X POST 'https://api.cozify.fi/ui/0.2/user/emaillogin' \
  -H 'Content-Type: application/json' \
  -d '{"email":"developer@example.com","password":"P7BQ8E"}'
```

#### Cloud token lifecycle

Client implementations should treat the returned cloud token as a long-lived client session token:

1. After successful login, the cloud token is valid for 4 weeks by default.
2. The token should be stored in client-side persistent storage so the user session can survive client restarts when that is appropriate for the platform. E.g. a browser based client could store the token in `localStorage`.
3. The client should read the token expiry from the token and refresh the token before it expires.
4. A practical refresh policy is to refresh when less than 2 weeks of validity remain. This is an example tradeoff between refresh frequency and the possibility that the user becomes inactive for a longer period.
5. Refresh uses the cloud endpoint `GET https://api.cozify.fi/ui/0.2/user/refreshsession` with the current token in the `Authorization` header.
6. If refresh succeeds, persist the new token in client-side storage and then explicitly invalidate the old token by calling `PUT https://api.cozify.fi/ui/0.2/user/logout` with the old token in the `Authorization` header.

Important implementation note:

- when invalidating the old token after refresh, the logout request must use the old token explicitly, not the newly refreshed token

Example cloud token refresh request:

```http
GET https://api.cozify.fi/ui/0.2/user/refreshsession
Authorization: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'https://api.cozify.fi/ui/0.2/user/refreshsession' \
  -H 'Authorization: eyJhbGciOi...'
```

Logging out invalidates the cloud token presented in the `Authorization` header so that it can no longer be used for cloud API calls. This is the normal way to revoke an active cloud-authenticated session, and it is also the mechanism clients should use to invalidate the previous token after a successful refresh.

Example cloud logout request:

```http
PUT https://api.cozify.fi/ui/0.2/user/logout
Authorization: eyJhbGciOi...
```

Shell example:

```bash
curl -X PUT 'https://api.cozify.fi/ui/0.2/user/logout' \
  -H 'Authorization: eyJhbGciOi...'
```

### 2.4 Getting HubKeys

A HubKey is not created locally by the API client. It is obtained from Cozify cloud services after the user has authenticated there. The flow for key retrieval is:

1. Authenticate the user with Cozify cloud services and obtain a cloud token.
2. Call `GET https://api.cozify.fi/ui/0.2/user/hubkeys` with the cloud token in the Authorization header.
3. Receive a set of HubKeys representing the Hubs the authenticated user is allowed to access.
4. Select the HubKey that corresponds to the target Hub and use that HubKey when making local Hub API calls.

Example request for listing HubKeys:

```http
GET https://api.cozify.fi/ui/0.2/user/hubkeys
Authorization: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'https://api.cozify.fi/ui/0.2/user/hubkeys' \
  -H 'Authorization: eyJhbGciOi...'
```

Example response:

```json
{
  "c93c7700-dc76-11e4-b1ab-84eb18b297c0": "eyJ0eXAiO...",
  "675ec9fe-3af7-11ec-9f96-f8dc7a35d413": "eyJ0eXAiO..."
}
```

The response body is a JSON object where each key is a `hub_id` and each value is the corresponding HubKey token for that Hub.

The cloud token authenticates the user to Cozify cloud services. The returned HubKeys are Hub-specific credentials for subsequent Hub access.

The `Authorization` header uses the raw token value. No `Bearer` prefix is used.

## 3. Hub discovery

Before calling the versioned command API, a client must locate the Hub and determine the correct API version. Discovery is therefore a bootstrap flow rather than a single endpoint call.

### 3.1 Discovering the Hub on the local network

- Find the Hub from local network management tools such as the router or DHCP administration interface.
- Use Zeroconf or Bonjour discovery on the local network.
- Use the cloud-assisted LAN lookup endpoint `GET https://api.cozify.fi/ui/0.2/hub/lan_ip` to retrieve LAN addresses of reachable Hubs associated with the current public network.

Example cloud-assisted LAN discovery request:

```http
GET https://api.cozify.fi/ui/0.2/hub/lan_ip
Authorization: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'https://api.cozify.fi/ui/0.2/hub/lan_ip' \
  -H 'Authorization: eyJhbGciOi...'
```

Example response:

```json
[
  "192.168.1.128",
  "192.168.1.107"
]
```

The response body is a JSON array of LAN IP address strings for reachable Hubs.

### 3.2 Reading Hub metadata

After the Hub address is known, the client should call a Hub metadata bootstrap endpoint. For local access this is
`GET /hub`. For remote-only access through the cloud relay, this is `GET /hub/remote/hub`.

These endpoints are the natural discovery pivot because they return the metadata needed to verify the target Hub and
initialize the client.

The metadata includes at least the Hub identifier, software version, state, name, and feature set. In practice, a client uses this endpoint to answer five questions: which Hub was reached, what is its lifecycle state, what name should be shown to the user, which API version should be used for subsequent calls, and which Hub features are available.

```http
GET http://192.168.1.42:8893/hub
```

Remote bootstrap equivalent:

```http
GET https://api.cozify.fi/ui/0.2/hub/remote/hub
Authorization: eyJhbGciOi...
X-Hub-Key: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'http://192.168.1.42:8893/hub'
```

The local Hub API port is fixed to `8893`.

The metadata returned by these bootstrap endpoints includes at least the following fields:

| Field | Meaning |
| --- | --- |
| `hubId` | Unique identifier of the Hub. |
| `name` | Human-readable Hub name. |
| `state` | Hub lifecycle state, such as `factory_new` or `claimed`. |
| `version` | Hub software version used to derive the API version prefix. |
| `connected` | Boolean indicating whether the Hub is currently connected to Cozify cloud services. |
| `features` | Sorted list of enabled Hub feature identifiers as integers. |

Example metadata response:

```json
{
  "hubId": "b40a33b4-23b4-14e5-925a-78c90baba039",
  "name": "Home Hub",
  "state": "claimed",
  "version": "1.14.12.20",
  "connected": true,
  "features": [
    23
  ]
}
```

`features` is not a list of strings. It is a list of numeric feature identifiers. Examples include:

- `10` = `BACNET`
- `12` = `ZWAVE`
- `15` = `MODBUS`
- `23` = `BACNET_SERVER`
- `24` = `BACNET_CLIENT`
- `32` = `BLE`

### 3.3 Deriving the API version

Most Hub API paths are versioned. The versioned path prefix is derived from the Hub software version returned by the
metadata bootstrap call, either `GET /hub` locally or `GET /hub/remote/hub` remotely. The API version is formed from
the first two version components.

Example: if the Hub version is `1.14.12.20`, the corresponding API version prefix is `1.14`.

A versioned request path then takes the form:

```text
http://192.168.1.42:8893/cc/1.14/devices
```

### 3.4 Recommended bootstrap sequence

1. Obtain the list of accessible Hubs from HubKeys.
2. If LAN candidate addresses are available, probe them with local `GET /hub`.
3. For any hub that is not yet confirmed locally, probe it remotely with `GET /hub/remote/hub` using the cloud token and the selected HubKey.
4. Read the returned `hubId`, `name`, `state`, `version`, and `features`.
5. Derive the API version from the software version.
6. Use the selected HubKey for local calls, or use the cloud token together with `X-Hub-Key` for remote calls.

This sequence is suitable for both human-operated clients and machine clients. It provides enough information to select the correct Hub deterministically, build the correct versioned path prefix, and choose the appropriate access mode for local or remote communication, including hubs that are reachable only through the remote relay.

#### Example sequence: local bootstrap and API prefix selection

```mermaid
sequenceDiagram
    autonumber
    actor Client
    participant Hub

    Client->>Hub: GET /hub
    Hub-->>Client: HUB_META_DATA {hubId, version, state, features, ...}
    Note over Client: Read hubId, lifecycle state, and API version
    Note over Client: Build versioned prefix /cc/<api-version>/...
    Client->>Hub: GET /cc/<api-version>/devices
    Hub-->>Client: {deviceId: deviceData, ...}
```

### 3.5 Local and remote access

For local access, the client communicates directly with the Hub on the local network and places the selected HubKey in the Authorization header.

```http
GET http://192.168.1.42:8893/cc/1.14/devices
Authorization: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'http://192.168.1.42:8893/cc/1.14/devices' \
  -H 'Authorization: eyJhbGciOi...'
```

For remote access, the client first fetches Hub metadata through the cloud bootstrap endpoint and then sends versioned
requests to the Cozify cloud relay endpoint. In that case, the Authorization header carries the cloud token and the HubKey is sent separately in the `X-Hub-Key` header.

Remote metadata bootstrap:

```http
GET https://api.cozify.fi/ui/0.2/hub/remote/hub
Authorization: eyJhbGciOi...
X-Hub-Key: eyJhbGciOi...
```

After the client has read `version` from that response and derived the API version prefix, normal remote calls use:

```http
GET https://api.cozify.fi/ui/0.2/hub/remote/cc/1.14/devices
Authorization: eyJhbGciOi...
X-Hub-Key: eyJhbGciOi...
```

Shell example:

```bash
curl -X GET 'https://api.cozify.fi/ui/0.2/hub/remote/cc/1.14/devices' \
  -H 'Authorization: eyJhbGciOi...' \
  -H 'X-Hub-Key: eyJhbGciOi...'
```

This distinction matters for client implementations: local access uses the HubKey as the primary authorization credential against the Hub itself, while remote access uses the cloud token for the relay and the HubKey to identify and authorize the target Hub session.

The `Authorization` header and `X-Hub-Key` header both use raw token values. No `Bearer` prefix is used.

#### Example sequence: remote access and HubKey usage

```mermaid
sequenceDiagram
    autonumber
    actor Client
    participant Cloud
    participant Hub

    Client->>Cloud: Request one-time password for email
    Cloud-->>Client: OTP delivered out-of-band
    Client->>Cloud: Log in with email + OTP
    Cloud-->>Client: Cloud token
    Client->>Cloud: Request HubKeys using cloud token
    Cloud-->>Client: HubKey list
    Note over Client: Select target Hub and matching HubKey
    Client->>Cloud: GET /hub/remote/hub with cloud token + HubKey
    Cloud->>Hub: Relay metadata bootstrap request
    Hub-->>Cloud: HUB_META_DATA {hubId, version, state, features, ...}
    Cloud-->>Client: HUB_META_DATA
    Note over Client: Derive /cc/<api-version>/... from version
    Client->>Cloud: Remote Hub API call with selected HubKey
    Cloud->>Hub: Relay request via websocket
    Hub-->>Cloud: Hub response
    Cloud-->>Client: Relayed Hub response
```

### 3.6 HubKey content and Hub selection

A HubKey carries enough Hub-specific information to support client-side Hub selection and request authorization. The payload includes the following fields that are particularly useful for connection management:

| Field | Description |
| --- | --- |
| `hub_id` | Unique identifier of the target Hub. This is the primary stable value for matching a HubKey to a discovered Hub. |
| `hub_name` | Human-readable Hub name. Suitable for display in a Hub picker or account-level list of accessible Hubs. |
| `user_id` | Identifier of the user in the context of the Hub. Useful for user-scoped diagnostics and audit-oriented client logic. |
| `email` | Email address associated with the HubKey owner. |
| `nickname` | Optional user nickname that can be used in user-aware clients. |
| `role` | Effective role on the Hub. Clients can use this value to predict whether administrative operations are likely to succeed. |
| `owner` | Boolean owner flag. Useful when a client needs to distinguish the Hub owner from other administrators. |

Because a HubKey contains both `hub_id` and `hub_name`, a list of HubKeys returned from the cloud can be transformed into a user-selectable list of accessible Hubs. The recommended client pattern is to decode each HubKey, extract at least `hub_id`, `hub_name`, and `role`, and present those values in the Hub selection UI or in machine-readable connection metadata.

Once a Hub has been identified through local `GET /hub` or remote `GET /hub/remote/hub`, the returned `hubId` can be matched against the `hub_id` field in the available HubKeys. This provides a deterministic way to choose the correct HubKey for that Hub.

HubKeys are client-decodable and may be decoded by the client for Hub selection and connection management. In particular, `hub_id`, `hub_name`, and `role` are useful for matching a discovered Hub to the correct HubKey and for presenting available Hubs in the client UI.

### 3.7 User role and access rights

Hub access is role-based. The effective role is carried by the HubKey and is used by the Hub when authorizing each API request. In normal operation, Hub roles are granted by the owner of the Hub.

Authorization is based on a bitwise check between role and access right:

```text
Role.USER_ADMIN & AccessRight.IDENTIFIED_USER_ACCESS > 0 -> can access
Role.IDENTIFIED_USER & AccessRight.USER_ADMIN_ACCESS == 0 -> can not access
```

The access check is:

```text
role & access > 0
```

#### Roles

The role set is:

| Constant | Value | Meaning |
| --- | --- | --- |
| `OWNER_ADMIN` | `0x40` | Owner of the Hub. |
| `USER_ADMIN` | `0x20` | User admin access on the Hub. |
| `IDENTIFIED_USER` | `0x08` | Identified user access. |
| `REMOTE_GUEST` | `0x04` | Remote guest access. |
| `GUEST` | `0x02` | Guest access. |
| `ANONYMOUS` | `0x01` | Anonymous access. |

The role names are:

| Role constant | Role name |
| --- | --- |
| `OWNER_ADMIN` | `owner` |
| `USER_ADMIN` | `user admin` |
| `IDENTIFIED_USER` | `identified user` |
| `REMOTE_GUEST` | `remote guest` |
| `GUEST` | `guest` |
| `ANONYMOUS` | `anonymous` |

For human-facing Hub use, the most relevant Hub roles are:

- **Owner**: the one and only owner of a claimed Hub.
- **User Admin**: full access but not the owner.
- **Identified User**: restricted access.
- **Remote Guest**: read-only access which can access the Hub remotely.
- **Guest**: even more restricted access. Guest access is available in the local area network only.

#### Role labels and role detection

For human-facing Hub settings and invitation flows, the roles are typically presented with the following labels:

| Role value | Display label |
| --- | --- |
| `32` | `Admin` |
| `8` | `User` |
| `4` | `Remote guest` |
| `2` | `Guest` |
| `1` | `Anonymous` |

The `owner` flag is tracked separately from the numeric role. In practice, an owner is shown as `Owner`
instead of `Admin`, even though the effective management role is administrative.

The current effective role for the selected Hub can be verified directly from the Hub with:

```http
GET /hub/keycheck
Authorization: eyJhbGciOi...
```

The response contains at least the effective `role`. Clients can use this to derive the user-facing role name.

#### Access rights

The access-right set is:

| Constant | Value | Meaning |
| --- | --- | --- |
| `OWNER_ADMIN_ACCESS` | `0xc0` | `owner access` |
| `USER_ADMIN_ACCESS` | `0xe0` | `user admin access` |
| `IDENTIFIED_USER_ACCESS` | `0xf8` | `identified user access` |
| `REMOTE_GUEST_ACCESS` | `0xfc` | `remote guest access` |
| `GUEST_ACCESS` | `0xfe` | `guest access` |
| `ANONYMOUS_ACCESS` | `0xff` | `anonymous access` |

Current user role should be matched against endpoint's documented access right mask.

#### Managing Hub users, roles, and invitations

Hub access rights are managed through Cozify cloud user-authorization endpoints.

The practical model is:

1. Authenticate to the cloud and select a Hub.
2. Use `GET /hub/keycheck` against the Hub to detect the current caller's effective role on that Hub.
3. If the caller has administrative rights, use the cloud authorization endpoints to list users, update roles, revoke
   access, or invite new users to that Hub.

These operations are Hub-scoped because the selected Hub id is always part of the request.

##### Listing current Hub users

The current access list for a Hub is read with:

```http
GET https://api.cozify.fi/ui/0.2/user/authorization?hubId=<hubId>
Authorization: eyJhbGciOi...
```

This returns the users who currently have access to the selected Hub together with their effective Hub roles and owner
status.

##### Updating an existing user's role

To change a user's role on one Hub, send:

```http
POST https://api.cozify.fi/ui/0.2/user/authorization
Content-Type: application/json
Authorization: eyJhbGciOi...

{
  "hubId": "hub-id",
  "email": "user@example.com",
  "role": 8
}
```

The `role` value is one of the numeric Hub role values, typically `32`, `8`, `4`, or `2`.

##### Revoking a user's access

To remove a user's access to one Hub, send:

```http
DELETE https://api.cozify.fi/ui/0.2/user/authorization
Content-Type: application/json
Authorization: eyJhbGciOi...

{
  "hubId": "hub-id",
  "email": "user@example.com"
}
```

This revokes that user's access to the selected Hub. The same endpoint is also used when the current user revokes
their own access.

##### Inviting a new user

To invite a new user to a Hub, send:

```http
POST https://api.cozify.fi/ui/0.2/user/invite
Content-Type: application/json
Authorization: eyJhbGciOi...

{
  "email": "new.user@example.com",
  "role": 2,
  "hubId": "hub-id"
}
```

Important invite behavior:

- invitations send email to the specified address, so clients should present this action carefully and avoid triggering it accidentally
- the invite is Hub-specific because it includes `hubId`
- the role is chosen at invite time
- after accepting the invitation and logging in, the invited user gains access to that Hub with the invited role
- invitation flows typically allow inviting `Admin`, `User`, `Remote guest`, or `Guest`, while `Anonymous` is not
  offered in normal UI flows

##### Recommended client behavior

- treat role management and invitation as cloud-authenticated account operations, not as ordinary local Hub API calls
- use the Hub id of the currently selected Hub in all cloud authorization and invitation requests
- treat the owner flag separately from the numeric role when presenting the current user in UI

#### How endpoint authorization works

API endpoints declare a required minimum access level. These access levels are checked against the role carried by the caller's HubKey using the bitmask rule above.

In practice, this means:

- Endpoints marked with `ANONYMOUS_ACCESS` can be used without a normal authenticated Hub session for selected bootstrap operations.
- Endpoints marked with `IDENTIFIED_USER_ACCESS` require an authenticated user with at least Identified User rights.
- Endpoints marked with `REMOTE_GUEST_ACCESS` require remote guest rights.
- Endpoints marked with `GUEST_ACCESS` require guest rights.
- Endpoints marked with `USER_ADMIN_ACCESS` require administrative rights on the Hub.
- Endpoints marked with `OWNER_ADMIN_ACCESS` require owner-level rights.

The access model is cumulative through the bitmask definitions: higher-privilege roles can access endpoints intended for lower-privilege roles when the bitwise access check succeeds, subject to normal request validation.

Examples:

- `GET /hub` is exposed with `ANONYMOUS_ACCESS`.
- Device operations commonly use `IDENTIFIED_USER_ACCESS`.
- Most system-level management operations use `USER_ADMIN_ACCESS`.
- Owner-only operations use `OWNER_ADMIN_ACCESS`.

Clients should treat the `role` value contained in the HubKey as the primary indicator of what operations are likely to succeed. A practical client strategy is to use that role both for request authorization and for UI behavior, such as hiding or disabling actions that require stronger privileges.

## Part 2 - Workflows, API calls, and shared API data structures

## 4. Workflows

### 4.1 Device pairing workflow

Device pairing is a scan-driven workflow.

The required sequence is:

1. Start pairing by calling `GET /hub/scan` with `ts=0`.
2. Keep pairing active by continuing to call `GET /hub/scan` while the pairing UI is open.
3. Read the returned scan results and show the discovered devices to the user.
4. Select which devices should be paired by calling `PUT /hub/scan` for the discovered device entries.
5. Stop pairing by calling `GET /hub/stopscan`.

The scan is started automatically by the first `GET /hub/scan` call when no scan is active.

While pairing is active, the client should continue polling `GET /hub/scan`:

- on local connections, each call refreshes a timeout of approximately 10 seconds
- on remote connections, each call refreshes a timeout of approximately 60 seconds

If the client stops polling and the timeout expires, pairing is stopped automatically.

`GET /hub/scan` returns a scan delta containing discovered devices. Each discovered device entry can include:

- device identifier
- `ignored` state
- pairing status
- status data
- optional user-facing pairing help text in `info`
- `actionRequired` to indicate that user action is still needed

Device selection is controlled through `PUT /hub/scan`. The selection model is based on the `ignored` flag:

- newly discovered devices are ignored by default
- set `ignored=false` for devices that should be paired
- set `ignored=true` for devices that should not be paired

In practice, this means it is enough for the client to change `ignored` to `false` only for those discovered devices that the user has selected for pairing.

When pairing is stopped, devices marked as ignored are discarded and devices not marked as ignored are finalized as paired devices.

Some device types may require additional user action during pairing. In scan results this is indicated by:

- `actionRequired=true`
- explanatory text in `info`

The client should present this information to the user while continuing to poll scan results until pairing completes or is stopped.

Z-Wave pairing has an additional manual inclusion step while normal Hub pairing is active. The Z-Wave operations use
`POST /hub/protocolconfig` and are documented in chapter 8.

Modbus RTU pairing also has a manual protocol-command flow. Clients use `POST /hub/protocolconfig` during pairing to
read available Modbus pairing templates and to submit `PAIR_MODBUS` requests. That flow is documented in chapter 10.

#### Example sequence: device pairing

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/hub/scan?ts=0
    Hub-->>Client: SCAN_DELTA {full=true, devices=[...]}
    loop Keep pairing active
        Client->>Hub: GET /cc/<api-version>/hub/scan?ts=<last-scan-ts>
        Hub-->>Client: SCAN_DELTA {devices=[DEVICE_SCAN, ...]}
        Note over Client: Show info and actionRequired to user
    end
    User->>Client: Select devices to pair
    Client->>Hub: PUT /cc/<api-version>/hub/scan\nSET_SCAN_RESULT {id, ignored=false}
    Hub-->>Client: true
    Client->>Hub: GET /cc/<api-version>/hub/stopscan
    Hub-->>Client: true
    Note over Hub: Ignored devices are discarded\nSelected devices are finalized
```

#### 4.1.1 Configuring devices with `CONFIGURABLE` capability

Some paired devices expose the `CONFIGURABLE` capability. These devices support a separate configuration workflow after pairing.

When a newly paired device has `CONFIGURABLE` capability, the client should offer device-specific configuration to the user. The configuration workflow uses these endpoints:

- `GET /devices/configuration` to read the current configuration of the device
- `PUT /devices/configuration` to save configuration changes
- `DELETE /devices/configuration` to remove custom configuration and restore default values

The configuration read call takes the target `deviceId`. It can also request configuration templates that describe the configurable fields and their allowed values.

A typical editable configuration flow is:

1. Call `GET /devices/configuration` with `templates=true`.
2. Read `template.parameters` and render the configuration controls in that order.
3. For each parameter, use `description` as the primary label and `help` as supporting text.
4. Read the current value from `parameters[parameter.name]`. If there is no current value, initialize the control from `defaultValue`.
5. Choose the editor based on `fieldType`, enforce `minValue` and `maxValue` when applicable, and disable editing when `readOnly=true`.
6. Save the edited values with `PUT /devices/configuration` using the same device id.
7. Use `DELETE /devices/configuration` when the user wants to return to the device defaults.

Configuration is separate from normal device metadata and separate from operational commands. This means that:

- `PUT /devices` is used for metadata such as name and room
- `PUT /devices/command` is used for operational control
- `/devices/configuration` is used for persistent device-specific settings

Device configuration operations require user-admin access.

#### Example sequence: configurable device workflow

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/devices
    Hub-->>Client: {deviceId: deviceData}
    Note over Client: Detect CONFIGURABLE in status.capabilities
    Client->>Hub: GET /cc/<api-version>/devices/configuration?deviceId=<device-uuid>&templates=true
    Hub-->>Client: CONFIGURATION {template, parameters}
    Note over Client: Render fields in template.parameters order
    User->>Client: Edit values
    Client->>Hub: PUT /cc/<api-version>/devices/configuration\nCONFIGURATION {id, parameters}
    Hub-->>Client: true
    opt Restore defaults
        User->>Client: Reset configuration
        Client->>Hub: DELETE /cc/<api-version>/devices/configuration?deviceId=<device-uuid>
        Hub-->>Client: true
    end
```

#### 4.1.2 Configuring IO-module ports during pairing and later management

Some devices act as IO-module containers. A single paired module device can expose multiple configurable input and
output ports, and each selected port becomes its own child device. This flow is used by devices derived from the
Hub's IO-module base implementation, such as relay modules and other multi-channel input/output devices.

IO-module container's status.type is `FNIP`.
- use `PUT /devices/command` with `GetFnipConfigs`, `FnipMetaInputCommand`, `FnipMetaOutputCommand`,
  `ApplyConfiguration`, and `CancelConfiguration`

The container device stays as the management target for writes. `GET_FNIP_CONFIG` can return existing child-device ids
for already paired ports, but when sending `CMD_FNIP_INPUT`, `CMD_FNIP_OUTPUT`, `APPLY_CONFIG`, or `CANCEL_CONFIG`,
clients should set `id` to the module device id.

The read command is:

```json
[
  {
    "id": "<module-id>",
    "type": "GET_FNIP_CONFIG"
  }
]
```

The response is an `FNIP_CONFIG` object containing channel-ordered `inputs` and `outputs` arrays:

```json
{
  "id": "<module-id>",
  "type": "FNIP_CONFIG",
  "inputs": [
    {
      "id": "<child-or-module-id>",
      "type": "CMD_FNIP_INPUT",
      "channel": 1,
      "name": "Input 1",
      "deviceType": "WALLSWITCH",
      "paired": false,
      "normallyOff": true,
      "independent": false,
      "mode": 2,
      "canChange": true,
      "room": []
    }
  ],
  "outputs": [
    {
      "id": "<child-or-module-id>",
      "type": "CMD_FNIP_OUTPUT",
      "channel": 1,
      "name": "Relay 1",
      "deviceType": "POWER_SOCKET",
      "paired": false,
      "normallyOff": true,
      "canChange": true,
      "room": []
    }
  ]
}
```

For IO-module devices, this response is the working port map:

- if a channel already has a paired child device, the returned entry reflects that child's current configuration and can carry that child device's own `id`
- if a channel does not yet have a child device, the returned entry is the module's current default configuration for that slot
- arrays are in channel order and can contain `null` for unavailable slots

The relevant configuration fields are:

- `channel`: 1-based physical port number on the module
- `deviceType`: child device type to create for that port
- `paired`: whether that port should exist as a child device
- `name`, `room`: metadata for the child device created from that port
- `normallyOff`: input/output polarity flag; when `false`, the Hub inverts the raw on/off signal
- `canChange`: advisory flag from the Hub telling the client that the port type or behavior should be treated as fixed

The supported generic `deviceType` values are:

- input ports: `WALLSWITCH`, `MOTION`, `CONTACT`, `MOISTURE`, `DOORBELL`, `SMOKE_ALARM`, `TWILIGHT`
- output ports: `POWER_SOCKET`, `LIGHT`

These are the values accepted by the shared IO-module input/output type-switching implementation. A specific module or
specific channel can still support only a subset of them:

- defaults vary by driver and channel type
- some ports expose `canChange=false`, meaning the client should treat the port type as fixed
- some devices, such as dimmer or RGB-oriented modules, constrain the available output type further

Input entries also use these input-only fields:

- `independent`: when `true`, the input is not coupled to the module's local output
- `mode`: input behavior when the port is not independent
  - `1` = toggle
  - `2` = follow

Output entries do not use `independent` or `mode`.

For third-party clients, `independent` is the supported way to request an independent input. Do not send `mode=0`,
even though older code defines that legacy constant as "independent". Current input implementations treat `mode=0`
as invalid in normal client commands.

A typical pairing-time port configuration flow is:

1. Pair the module normally through `GET /hub/scan` and `PUT /hub/scan`.
2. While the scan session is still open, call `PUT /devices/command` with `GET_FNIP_CONFIG` for the discovered module.
3. Show the returned input and output entries to the user.
4. For each changed input port, send a `CMD_FNIP_INPUT` command to stage the desired configuration.
5. For each changed output port, send a `CMD_FNIP_OUTPUT` command to stage the desired configuration.
6. Stop pairing with `GET /hub/stopscan`.
7. When pairing completes, the Hub applies the staged port configuration automatically.

During initial pairing, no separate `APPLY_CONFIG` call is required. Staged IO-port changes are committed when pairing
ends successfully.

A typical already-paired reconfiguration flow is:

1. Read the current port map with `GET_FNIP_CONFIG`.
2. Send one or more `CMD_FNIP_INPUT` and `CMD_FNIP_OUTPUT` commands to stage the edited port settings.
3. Commit the staged changes with `APPLY_CONFIG`, or discard them with `CANCEL_CONFIG`.
4. Refresh devices or wait for the next poll delta to observe the resulting child-device changes.

Example input update:

```json
[
  {
    "id": "<module-id>",
    "type": "CMD_FNIP_INPUT",
    "channel": 1,
    "name": "Hall button",
    "deviceType": "WALLSWITCH",
    "paired": true,
    "normallyOff": true,
    "independent": false,
    "mode": 2,
    "room": [
      "room-uuid"
    ]
  }
]
```

Example output update:

```json
[
  {
    "id": "<module-id>",
    "type": "CMD_FNIP_OUTPUT",
    "channel": 1,
    "name": "Hall relay",
    "deviceType": "POWER_SOCKET",
    "paired": true,
    "normallyOff": true,
    "room": [
      "room-uuid"
    ]
  }
]
```

Example commit or rollback:

```json
[
  {
    "id": "<module-id>",
    "type": "APPLY_CONFIG"
  }
]
```

```json
[
  {
    "id": "<module-id>",
    "type": "CANCEL_CONFIG"
  }
]
```

The commit semantics are important:

- `paired=true` on a previously unused port creates a new child device for that channel
- `paired=false` on an existing child port removes that child device
- changing `deviceType` recreates the child device as the new type
- changing `name` or `room` updates the child metadata
- `CANCEL_CONFIG` discards staged edits and restores the current live port map

Port-management writes require user-admin access.

### 4.2 Device workflow

Device handling usually consists of listing devices, updating device metadata, sending commands, optionally configuring configurable devices, and removing devices when needed.

The available operations are:

- read the current devices with `GET /devices`
- update device metadata with `PUT /devices` when the user changes device details
- send operational commands with `PUT /devices/command`
- if the device type supports configuration, use the configuration workflow described in `4.1.1`
- optionally run presets with `PUT /devices/preset`
- delete devices with `DELETE /devices` when they should be removed from the Hub

The basic read endpoint is:

- `GET /devices` to fetch all devices.

If `groupId` is provided, `GET /devices` returns only the devices that belong to that group.

Device metadata updates use `PUT /devices`. This is used for user-facing device information such as:

- name
- room
- other editable device metadata

The request contains a list of device update commands. Some device management changes are more restricted than normal metadata updates:

- device access changes require owner-level access
- device visibility changes require owner-level access
- device lock state changes require owner-level access

Operational control uses `PUT /devices/command`. This endpoint is primarily asynchronous:

- normal device commands are dispatched and the request returns without waiting for the final device-side result
- `IgnoreDeviceCmd` and `ResetIgnoreCmd` are exceptions and return a synchronous success result

When the caller is a remote guest, device commanding is restricted to read-only style commands. Remote guest access cannot be used for general device control.

Presets can be triggered with `PUT /devices/preset`. For remote calls, presets require at least identified-user access.

If a device supports configurable settings, use the configuration workflow in `4.1.1`. Device configuration operations require user-admin access.

Devices can be removed with `DELETE /devices` by providing a list of device identifiers.

### 4.3 Group workflow

Groups are managed through a small set of operations: list existing groups, create or modify groups, send commands to groups, and delete groups.

The available operations are:

- read the current groups with `GET /groups`
- create a new group or modify an existing group with `PUT /groups`
- send commands to the group with `PUT /groups/command`
- delete the group with `DELETE /groups` when it is no longer needed

The basic read endpoint is:

- `GET /groups` to fetch all groups.

Group creation and modification use `PUT /groups`. The request contains a list of group modification commands.

To create a new group:

- set the group's `id` field to `null`

To modify an existing group:

- set the group's `id` field to the identifier of the existing group

The group modification command supports at least the following fields:

- `id`
- `name`
- `members`: list of device identifiers
- `room`: list of room identifiers, with one room supported

If an attribute in the group modification command is `null` or missing, it is not changed.

The response from `PUT /groups` is a list of modified groups. The returned group data does not include the `state` attribute.

After a group has been created, the client can send commands to it with `PUT /groups/command`. The supported command forms are:

- device command payloads for group members
- `GroupOnCommand`
- `GroupOffCommand`

Group command handling is asynchronous. The commands are dispatched to the group and the request returns without waiting for the final device-level outcome.

For remote calls, group commanding has a stricter access rule than local guest access:

- local calls may use `GUEST_ACCESS`
- remote calls require at least `IDENTIFIED_USER_ACCESS`

The client can delete a group with `DELETE /groups` by providing the target `groupId`.

### 4.4 Rule creation workflow

Rule creation is a multi-step workflow. A client typically needs to discover which rule types are available, render the selected rule template into a configuration form, create or update the rule configuration together with its on-configuration, and then enable or disable the rule as needed.

The common sequence is:

1. Read the available rule templates with `GET /rules/templates`.
2. Select one template and render its `inputs`, `outputs`, and `extras` as a rule configuration form.
3. Build a `RuleConfig` from the values chosen in that form.
4. Create or update the rule with `PUT /rules`, including its on-configuration.
5. If needed, change when the rule is on or off with `PUT /rules/onConfiguration`.
6. Send a rule on/off command with `PUT /rules/command`.

The basic discovery endpoints are:

- `GET /rules/templates` to list available rule templates.
- `GET /rules` to fetch existing rules, or a single rule when `ruleId` is provided.

Rule templates define the structure of the configuration form. The canonical field tables for `RuleTemplate`, `Input`, `Output`, `Value`, `RuleConfig`, and `RuleOnConfig` are in [6.10 Rule data](#610-rule-data), especially [Rule configuration objects](#rule-configuration-objects) and [Typed rule value objects](#typed-rule-value-objects).

This workflow chapter keeps only the high-level roles of those objects:

- `RuleTemplate` is returned by `GET /rules/templates` and defines the form structure.
- `Input`, `Output`, and `Value` are nested field descriptors inside a `RuleTemplate`.
- `RuleConfig` is the main configuration object sent to `PUT /rules`.
- `RuleOnConfig` defines when a rule is active and is sent to both `PUT /rules` and `PUT /rules/onConfiguration`.

When rendering a template:

- order fields by `displayOrder`
- use the field map key as the configuration key
- render `Input` and `Output` items as object pickers filtered by `fieldType` and `capabilities`
- treat `fieldType` values and capability names as exact identifiers
- do not assume capability inheritance or aliasing; for example, `SELF_MOTION` does not satisfy a field that requires `MOTION`
- enforce `minCount` and `maxCount`
- render `Value` items according to `fieldType`
- use `defaultValue`, `minValue`, `maxValue`, and `options` as editor constraints
- treat `readOnly=true` fields as informational or fixed values rather than editable fields

Rule creation and update use `PUT /rules`. The request contains a list of rule configurations together with `onConfiguration`. This call can be used both to create new rules and to update existing rules.

The configuration is generated from the selected template as follows:

- copy `template.configType` to `config.configType`
- use the chosen rule name as `config.name`
- store selected object ids into `config.inputs[fieldName]` and `config.outputs[fieldName]`
- store configured extra values into `config.extras[fieldName]`
- include every declared input and output field in the serialized `RuleConfig`, even when nothing is selected
- represent an unselected input or output field with an empty array `[]`
- send the resulting `RuleConfig` together with `onConfiguration`

The full field tables for `RuleConfig` and `RuleOnConfig` are intentionally documented only once, in [Rule configuration objects](#rule-configuration-objects).

Concrete `AutoLight` template example:

```json
{
  "type": "TEMPLATE",
  "category": "FACTORY_RULE",
  "description": "Lights on when movement",
  "summary": "When someone enters a room, the motion sensor detects their movement, and the light is put on. You may also specify how long the light is on after the room is vacant again.",
  "configType": "AUTO_LIGHT_RULE",
  "name": "Auto light",
  "priority": null,
  "icon": null,
  "inputs": {
    "motions": {
      "type": "INPUT",
      "fieldType": "MOTION",
      "capabilities": [],
      "displayOrder": 1,
      "description": "Detects movement",
      "help": null,
      "minCount": 1,
      "maxCount": 1000,
      "minValue": null,
      "maxValue": null,
      "defaultValue": null,
      "readOnly": false,
      "priority": true,
      "filter": null,
      "selectOp": "and",
      "icon": null
    },
    "twilights": {
      "type": "INPUT",
      "fieldType": null,
      "capabilities": ["TWILIGHT"],
      "displayOrder": 2,
      "description": "Twilight sensor",
      "help": null,
      "minCount": 0,
      "maxCount": null,
      "minValue": null,
      "maxValue": null,
      "defaultValue": null,
      "readOnly": false,
      "priority": true,
      "filter": null,
      "selectOp": "and",
      "icon": null
    }
  },
  "outputs": {
    "lights": {
      "type": "OUTPUT",
      "fieldType": "LIGHT",
      "capabilities": ["ON_OFF"],
      "displayOrder": 3,
      "description": "Turn on",
      "help": null,
      "minCount": 1,
      "maxCount": null,
      "minValue": null,
      "maxValue": null,
      "defaultValue": null,
      "readOnly": false,
      "priority": true,
      "filter": null,
      "selectOp": "and",
      "icon": null
    }
  },
  "extras": {
    "timeout": {
      "type": "VALUE",
      "fieldType": "TIME_DELTA_MS",
      "displayOrder": 4,
      "description": "Turn off after no movement",
      "help": null,
      "minCount": 0,
      "maxCount": 1,
      "minValue": null,
      "maxValue": null,
      "defaultValue": 300000,
      "options": null,
      "readOnly": false,
      "priority": true,
      "filter": null,
      "extraFor": null
    }
  }
}
```

Generated `AutoLight` configuration example:

```json
{
  "type": "CONFIG",
  "id": null,
  "name": "Hallway auto light",
  "configType": "AUTO_LIGHT_RULE",
  "inputs": {
    "motions": ["<motion-device-uuid>"],
    "twilights": ["<twilight-device-uuid>"]
  },
  "outputs": {
    "lights": ["<light-device-uuid>"]
  },
  "extras": {
    "timeout": [
      {
        "type": "TIME_DELTA_MS",
        "value": 300000
      }
    ]
  }
}
```

On-configuration can also be updated separately through `PUT /rules/onConfiguration`. This is used to change when the rule is on or off through scenes or timers.

A manual-only on-configuration is also valid. The payload `{"type":"RULE_ON_CFG","scenes":[],"timers":[]}` means the rule has no scene-based or timer-based auto-activation and is controlled only through explicit rule commands. In that mode, clients turn the rule on with `RULE_ON_COMMAND` and off with `RULE_OFF_COMMAND`.

After a rule has been created, the client can control its state with `PUT /rules/command`. The normal commands are:

- `RuleOnCommand`
- `RuleOffCommand`

The client can remove a rule with `DELETE /rules`.

For administrative workflows, there are also template installation endpoints:

- `POST /rules/template`
- `DELETE /rules/template`

These are intended for managing custom rules rather than normal rule creation. Their detailed behavior is documented in
[15. Custom rules](#15-custom-rules).

There are also two custom-rule inspection endpoints:

- `GET /rules/template`
- `GET /rules/log`

`GET /rules/template` returns the stored source code of one installed custom rule by rule type.

The custom-rule-only endpoints are:

- `GET /rules/log`
- `POST /rules/template`
- `DELETE /rules/template`

These endpoints require the Hub `CUSTOM_RULES` feature. Without that feature, they are not part of the normal rule workflow and the Hub returns an error if they are used.

Rule proposals are a distinct functionality available through `POST /rules/proposals`. They are typically used after new devices have been added to the system and the client wants the Hub to suggest suitable rules based on:

- selected devices
- room
- maximum number of returned proposals
- configuration type

#### Example sequence: rule creation from a template

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/rules/templates
    Hub-->>Client: [TEMPLATE, ...]
    Note over Client: Select template and render inputs, outputs, extras
    User->>Client: Choose devices and values
    Note over Client: Build CONFIG using template keys\nCopy template.configType to config.configType
    Client->>Hub: PUT /cc/<api-version>/rules\n[{config: CONFIG, onConfiguration: RULE_ON_CFG}]
    Hub-->>Client: [RULE, ...]
    opt Change when the rule is active later
        Client->>Hub: PUT /cc/<api-version>/rules/onConfiguration\nRULE_ON_CFG
        Hub-->>Client: RULE_ON_CFG
    end
```

### 4.5 Scene workflow

Scenes are reusable sets of device preset states. When a scene is turned on, the Hub applies the scene's stored presets to the devices that belong to that scene.

Scenes are used to create named states such as home, away, evening, or movie mode. A scene can be activated manually by the user, and rules can also turn scenes on or off automatically.

Scenes can also be used as rule activation conditions. A rule can be configured so that it runs only while one or more selected scenes are on.

Scenes can also have timers. A timed scene is turned on automatically when its time range starts and turned off automatically when its time range ends.

Scenes are divided into user scenes and factory scenes:

- user scenes are created by the user
- factory scenes are built-in scenes provided by the Hub

Factory scenes cannot be removed or renamed.

This means that a scene can be used in several ways at the same time:

- as a manually activated named state
- as a timed state that follows configured start and stop times
- as a condition that determines when rules are allowed to run

The available operations are:

- read the current scenes with `GET /scenes`
- create a new scene or modify an existing scene with `PUT /scenes` or `PUT /scenes/command`
- turn a scene on or off with `PUT /scenes/command`
- delete a scene with `DELETE /scenes`

The basic read endpoint is:

- `GET /scenes` to fetch all scenes

If `sceneId` is provided, `GET /scenes` returns only that scene.

Scene creation and modification use `ChangeScene` commands. The canonical `Scene`, `Preset`, and scene command definitions are in [6.11 Scene data](#611-scene-data), especially [Scene read objects](#scene-read-objects) and [Scene command objects](#scene-command-objects). To create a new scene, set the scene `id` to `null`. If a scene with the given `id` already exists, it is modified.

Scene modification supports at least these fields:

- `id`
- `name`
- `isOn`
- `presets`
- `sceneTimes`

If an attribute is `null` or missing from the change command, it is not changed.

`sceneTimes` defines the time ranges when the scene should automatically be on. If timers are configured for a scene, the Hub manages the scene's on/off state according to those timers.

Scene commands use `PUT /scenes/command`. The supported command forms are:

- `ChangeScene`
- `SceneOnCommand`
- `SceneOffCommand`

For remote calls, commanding scenes requires at least identified-user access.

Scenes can be removed with `DELETE /scenes` by providing the target `sceneId`.

#### Example sequence: scene-controlled rule activation

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    User->>Client: Turn scene on
    Client->>Hub: PUT /cc/<api-version>/scenes/command\nCMD_SCENE_ON
    Hub-->>Client: true
    Note over Hub: Scene state changes
    Note over Hub: Rules whose RULE_ON_CFG.scenes contain the scene will turn on
    User->>Client: Turn scene off
    Client->>Hub: PUT /cc/<api-version>/scenes/command\nCMD_SCENE_OFF
    Hub-->>Client: true
```

### 4.6 Room functionality

Rooms are used to organize the Hub into named locations such as living room, bedroom, or kitchen.

Rooms provide a user-facing structure for devices and other Hub content. A room has a name and an ordering value that controls how rooms are presented in clients.

The available operations are:

- read the current rooms with `GET /rooms`
- create a new room or modify an existing room with `PUT /rooms`
- change room ordering with `PUT /rooms/order`
- delete a room with `DELETE /rooms`

The basic read endpoint is:

- `GET /rooms` to fetch all rooms

If `roomId` is provided, `GET /rooms` returns only that room.

Room creation and modification use room commands.

To create a new room:

- set the room `id` to `null`

To modify an existing room:

- set the room `id` to the identifier of the existing room

The room data includes at least:

- `id`
- `name`
- `order`

Room ordering is managed separately through `PUT /rooms/order`.

Rooms can be removed with `DELETE /rooms` by providing the target `roomId`.

Deleting a room removes the room itself. Clients should not assume that devices, groups, or rules will continue to refer to that room afterward.

### 4.7 Alarm functionality

Alarms represent active or historical alarm conditions in the Hub.

Alarm data can be used to show the user what has happened, whether an alarm is still open, and when it was created. Alarms may originate from Hub logic, devices, or integrations.

An alarm contains at least:

- `id`
- `sourceId`
- `title`
- `name`
- `message`
- `timestamp`
- `createdAtMs`
- `level`
- `closed`

Alarm `level` indicates severity. The available levels are:

- `info`
- `warn`
- `err`

The available operations are:

- read alarms with `GET /alarms`
- close an alarm with `PUT /alarms/close`
- delete an alarm with `DELETE /alarms`

The basic read endpoint is:

- `GET /alarms` to fetch all alarms

If `alarmId` is provided, `GET /alarms` returns only that alarm.

`PUT /alarms/close` marks an alarm as closed without deleting it.

`DELETE /alarms` permanently removes the target alarm.

## 5. API calls

### 5.1 General Hub endpoints

The Hub metadata bootstrap endpoints are unversioned:

- local: `GET /hub`
- remote relay: `GET /hub/remote/hub`

The other general Hub endpoints are versioned and are exposed under `/cc/<api-version>/hub/...`.

#### 5.1.1 `GET /hub`

**Access:** `ANONYMOUS_ACCESS`

Gets Hub metadata.

This endpoint is the primary local bootstrap entry point. It is used to identify the target Hub, determine its lifecycle state, and derive the versioned API prefix.

This endpoint is available only as unversioned `GET /hub`. It is not part of the versioned `/cc/<api-version>/hub/...` endpoint set.

For remote-only bootstrap through the cloud relay, use `GET /hub/remote/hub` with:

- `Authorization: <cloud-token>`
- `X-Hub-Key: <hub-key>`

`GET /hub/remote/hub` returns the same Hub metadata object and is the documented way to bootstrap remote-only Hub selection before the client knows which `/hub/remote/cc/<api-version>/...` prefix to use.

Response:

- Hub metadata object

Example response:

```json
{
  "type": "HUB_META_DATA",
  "hubId": "<hub-uuid>",
  "name": "Example Hub",
  "version": "1.14.12.20",
  "state": "claimed",
  "connected": true,
  "features": [
    5,
    15,
    23
  ]
}
```

#### 5.1.2 `POST /hub/claim`

**Access:** `ANONYMOUS_ACCESS`

Claims a factory-new Hub for the given email address. This endpoint is only valid while the Hub is in the `factory_new` state.

Request parameters:

- `email`: email address used for claiming the Hub

Example request:

```http
POST /cc/<api-version>/hub/claim?email=user@example.com
```

#### 5.1.3 `PUT /hub/name`

**Access:** `USER_ADMIN_ACCESS`

Sets the Hub name. This endpoint can also set the house type in the same request.

Request parameters:

- `name`: new Hub name
- `housetype`: optional house type value

Example request:

```http
PUT /cc/<api-version>/hub/name?name=Example%20Hub&housetype=house
```

#### 5.1.4 `GET /hub/poll`

**Access:** `GUEST_ACCESS`

Polls the Hub for data.

This endpoint returns Hub data which has changed since the given timestamp parameters. It is the main endpoint for incremental synchronization.

Recommended usage:

1. Call `GET /hub/poll?ts=0` for the initial synchronization.
2. For each subsequent poll, set `ts` to the timestamp returned by the previous poll response.
3. Use only the `ts` and `cozify_uuid` query parameters in normal clients.
4. When polling the Hub over LAN, send the next poll about two seconds after receiving the previous response.
5. When polling the Hub remotely through the cloud service, send the next poll about five seconds after receiving the previous response.

Example:

```text
time    request     response
        timestamp   timestamp
-----------------------------
200          0         200
202        200         202
204        202         204
```

All timestamps are milliseconds since EPOCH.

`cozify_uuid` should identify the client application instance. The recommended value is a 16-byte UUID for that installation or app instance. The field is mainly used for notification routing. Any string value is accepted, but clients should keep it stable for the same installation.

Request parameters:

- `ts`: timestamp for all data. If specified, the result contains all Hub data that has changed after this timestamp.
- `cozify_uuid`: optional client identifier. Recommended for clients that maintain a stable application-instance identifier.

Legacy or specialized timestamp parameters exist (`deviceTs`, `groupTs`, `sceneTs`, `ruleTs`, `userTs`, `alertTs`, `roomTs`, `zoneTs`, `alarmTs`, `activatorTs`), but the recommended client pattern is to use only `ts`.

Response:

- single delta object

Example sequence:

```mermaid
sequenceDiagram
    autonumber
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/hub/poll?ts=0
    Hub-->>Client: DELTA {timestamp=t1, full=true, ...}
    Note over Client: Store returned timestamp t1
    loop Incremental sync
        Client->>Hub: GET /cc/<api-version>/hub/poll?ts=t1
        Hub-->>Client: DELTA {timestamp=t2, full=false, polls=[...]}
        Note over Client: Apply changes and replace t1 with t2
    end
```

#### 5.1.5 `GET /hub/colors`

**Access:** `GUEST_ACCESS`

Returns the stored color list used by the Hub UI.

Example response:

```json
[
  "#ff0000",
  "#00ff00",
  "#0000ff"
]
```

#### 5.1.6 `POST /hub/colors`

**Access:** `GUEST_ACCESS`

Stores a color value in the Hub color list.

Request parameters:

- `color`: color value to store

Example request:

```http
POST /cc/<api-version>/hub/colors?color=%23ff0000
```

#### 5.1.7 `DELETE /hub/colors`

**Access:** `GUEST_ACCESS`

Clears the stored color list.

Example request:

```http
DELETE /cc/<api-version>/hub/colors
```

#### 5.1.8 `POST /hub/zigbeechannel`

**Access:** `USER_ADMIN_ACCESS`

Changes the Zigbee channel.

Request parameters:

- `channel`: target Zigbee channel
- allowed values: integers `11` through `26`

Example request:

```http
POST /cc/<api-version>/hub/zigbeechannel?channel=15
```

#### 5.1.9 `POST /hub/zigbeepower`

**Access:** `USER_ADMIN_ACCESS`

Changes the Zigbee radio power.

Request parameters:

- `power`: target Zigbee transmission power
- allowed values: integers `3` through `8`

Example request:

```http
POST /cc/<api-version>/hub/zigbeepower?power=8
```

#### 5.1.10 `GET /hub/zigbeestatus`

**Access:** `GUEST_ACCESS`

Returns current Zigbee network status information.

Example response:

```json
{
  "type": "ZIGBEE_NETWORK_STATUS",
  "connected": true,
  "has_network": true,
  "channel": 15,
  "power": 8,
  "firmware": "6.10.3",
  "stats": {
    "type": "ZIGBEE_RADIO_STATS",
    "rx_successful": 1250,
    "rx_err_none": 0,
    "rx_err_invalid_frame": 0,
    "rx_err_invalid_fcs": 0,
    "rx_err_invalid_dest_addr": 0,
    "rx_err_runtime": 0,
    "rx_err_timeslot_ended": 0,
    "rx_err_aborted": 0,
    "tx_successful": 980,
    "tx_err_none": 0,
    "tx_err_busy_channel": 2,
    "tx_err_invalid_ack": 0,
    "tx_err_no_mem": 0,
    "tx_err_timeslot_ended": 0,
    "tx_err_no_ack": 1,
    "tx_err_aborted": 0,
    "tx_err_timeslot_denied": 0
  },
  "ignore_reachability": false
}
```

#### 5.1.11 `POST /hub/zigbeesettings`

**Access:** `USER_ADMIN_ACCESS`

Sets Zigbee network settings.

Request body:

- Zigbee network settings object

Example request:

```json
{
  "type": "ZIGBEE_NETWORK_SETTINGS",
  "channel": 15,
  "power": 8,
  "ignore_reachability": false
}
```

#### 5.1.12 `GET /hub/zigbeescan`

**Access:** `USER_ADMIN_ACCESS`

Runs a Zigbee energy scan and returns the result.

Example request:

```http
GET /cc/<api-version>/hub/zigbeescan
```

#### 5.1.13 `POST /hub/pairphilips`

**Access:** `USER_ADMIN_ACCESS`

Starts pairing of a Philips bridge integration.

Example request:

```http
POST /cc/<api-version>/hub/pairphilips
```

#### 5.1.14 `POST /hub/pairairpatrol`

**Access:** `USER_ADMIN_ACCESS`

Pairs an Airpatrol device by hardware identifier.

Request parameters:

- `hwId`: Airpatrol hardware identifier

The `hwId` can be read from the QR code on the physical Airpatrol device.

Example request:

```http
POST /cc/<api-version>/hub/pairairpatrol?hwId=AP12345678
```

#### 5.1.15 `POST /hub/lockconfig`

**Access:** `USER_ADMIN_ACCESS`

Locks Hub configuration.

This endpoint applies a lock access mask to every device so that configuration rights are removed while read and
command rights remain available. After the access masks are updated, the Hub schedules the locked state to be
persisted as a user backup.

Example request:

```http
POST /cc/<api-version>/hub/lockconfig
```

Example response:

```json
true
```

#### 5.1.16 `GET /hub/users`

**Access:** `GUEST_ACCESS`

Returns Hub users.

Example response:

```json
{
  "2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401": {
    "id": "2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401",
    "user_info": {
      "user": {
        "uid": "2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401",
        "email": "developer@example.com",
        "nickname": "Developer",
        "phone": null,
        "defaults": true
      },
      "role": 64
    },
    "revoked": false,
    "connected": true,
    "timestamp": 1710249600000
  }
}
```

#### 5.1.17 `GET /hub/tz`

**Access:** `GUEST_ACCESS`

Returns the current Hub timezone.

Response:

- timezone identifier as a string

Example response:

```json
"Europe/Helsinki"
```

#### 5.1.18 `PUT /hub/tz`

**Access:** `USER_ADMIN_ACCESS`

Sets the current Hub timezone.

Request parameters:

- `timezone`: timezone identifier

Example request:

```http
PUT /cc/<api-version>/hub/tz?timezone=Europe/Helsinki
```

Example response:

```json
"Europe/Helsinki"
```

#### 5.1.19 `GET /hub/housetypes`

**Access:** `ANONYMOUS_ACCESS`

Returns available house types.

Response:

- dictionary `{house type code: localized display name}`

Allowed house type codes:

- `block`
- `row`
- `pair`
- `house`
- `leisure`
- `industrial`
- `other`

Example response:

```json
{
  "block": "Apartment block",
  "row": "Row house",
  "pair": "Semi-detached house",
  "house": "House",
  "leisure": "Leisure home",
  "industrial": "Industrial building",
  "other": "Other"
}
```

#### 5.1.20 `GET /hub/housetype`

**Access:** `GUEST_ACCESS`

Returns the current house type.

Response:

- house type code as a string

Example response:

```json
"house"
```

#### 5.1.21 `PUT /hub/housetype`

**Access:** `USER_ADMIN_ACCESS`

Sets the current house type.

Request parameters:

- `housetype`: house type value

Allowed values:

- `block`
- `row`
- `pair`
- `house`
- `leisure`
- `industrial`
- `other`

Example request:

```http
PUT /cc/<api-version>/hub/housetype?housetype=house
```

Example response:

```json
"house"
```

#### 5.1.22 `GET /hub/timezones`

**Access:** `GUEST_ACCESS`

Returns available timezone identifiers.

Response:

- array of timezone identifier strings

Example response:

```json
[
  "Europe/Helsinki",
  "Europe/Stockholm"
]
```

#### 5.1.23 `GET /hub/433devices`

**Access:** `USER_ADMIN_ACCESS`

Returns available 433 MHz device classes.

Example request:

```http
GET /cc/<api-version>/hub/433devices
```

Example response:

```json
[
  {
    "type": "433_PAIR_INFO",
    "id": "PROOVE_MOTION",
    "name": "Proove motion sensor",
    "manufacturer": "Proove"
  },
  {
    "type": "433_PAIR_INFO",
    "id": "NEXA_SWITCH",
    "name": "Nexa power outlet",
    "manufacturer": "Nexa"
  }
]
```

#### 5.1.24 `POST /hub/pair433`

**Access:** `USER_ADMIN_ACCESS`

Starts pairing of a 433 MHz device class.

Request parameters:

- `device433`: 433 MHz device class identifier

Use one of the `id` values returned by `GET /hub/433devices`, for example `PROOVE_MOTION`, `NEXA_SWITCH`,
`NEXA_CONTACT`, or `AIRAM_SMOKEALARM`.

Example request:

```http
POST /cc/<api-version>/hub/pair433?device433=NEXA_SWITCH
```

#### 5.1.25 `GET /hub/scan`

**Access:** `USER_ADMIN_ACCESS`

Returns scan results for device discovery.

Request parameters:

- `ts`: optional scan timestamp filter

Example request:

```http
GET /cc/<api-version>/hub/scan?ts=0
```

Example response:

```json
{
  "type": "SCAN_DELTA",
  "timestamp": 1710249600000,
  "full": true,
  "devices": [
    {
      "type": "DEVICE_SCAN",
      "id": "<device-uuid>",
      "ignored": true,
      "info": "Press the pairing button on the device.",
      "actionRequired": true,
      "timestamp": 1710249600000
    }
  ]
}
```

#### 5.1.26 `PUT /hub/scan`

**Access:** `USER_ADMIN_ACCESS`

Updates the `ignored` state of a discovered scan result.

Request body:

- `SetScanResult` object containing the discovered device id and the desired `ignored` value

Example request:

```json
{
  "type": "SET_SCAN_RESULT",
  "id": "<device-uuid>",
  "ignored": false
}
```

#### 5.1.27 `GET /hub/stopscan`

**Access:** `USER_ADMIN_ACCESS`

Stops ongoing device scanning.

Example request:

```http
GET /cc/<api-version>/hub/stopscan
```

Example response:

```json
true
```

#### 5.1.28 `GET /hub/features`

**Access:** `GUEST_ACCESS`

Returns Hub features visible to the UI.

Example response:

```json
[
  5,
  15,
  23,
  24,
  32
]
```

#### 5.1.29 `POST /hub/activatefeature`

**Access:** `USER_ADMIN_ACCESS`

Activates a Hub feature by activation code.

Request parameters:

- `code`: feature activation code

Example request:

```http
POST /cc/<api-version>/hub/activatefeature?code=ABCD-EFGH-IJKL
```

#### 5.1.30 `POST /hub/pairIp`

**Access:** `USER_ADMIN_ACCESS`

Pairs an IP-based device using the supplied host data.

Request body:

- IP host/device description object

Example request:

```json
{
  "type": "FOSCAM_AT_IP",
  "ip": "192.168.1.50",
  "username": "admin",
  "password": "secret"
}
```

Response:

- `FoscamHostAtIpStatus` object

The response object uses serialized type `FOSCAM_HOST_AT_IP_STATUS` and includes:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for the response object.</td><td>string</td><td><code>FOSCAM_HOST_AT_IP_STATUS</code></td></tr>
<tr><td><code>code</code></td><td>Result code describing whether a supported Foscam camera was found and whether the credentials matched.</td><td>integer</td><td><code>11</code></td></tr>
<tr><td><code>message</code></td><td>Localized human-readable message derived from <code>code</code>.</td><td>string</td><td><code>Please enter camera 192.168.1.50 username and password.</code></td></tr>
<tr><td><code>host</code></td><td>Optional discovered host payload. Present only for successful raw detection before the Hub hides it from the normal API response.</td><td><code>FoscamHost</code> or null</td><td><code>null</code></td></tr>
</tbody></table>

Known `code` values:

<table>
<thead><tr><th>Code</th><th>Name</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td><code>CODE_OK</code></td><td>Supported Foscam camera found at the given IP with matching credentials.</td></tr>
<tr><td><code>1</code></td><td><code>CODE_OK_PAIRED</code></td><td>Supported camera found and it is already paired with the Hub.</td></tr>
<tr><td><code>10</code></td><td><code>CODE_NOT_SUPPORTED</code></td><td>A camera responded at that IP, but it is not supported as a Foscam device.</td></tr>
<tr><td><code>11</code></td><td><code>CODE_CREDENTIALS</code></td><td>The host looks like a Foscam camera but the provided credentials are missing or not valid.</td></tr>
<tr><td><code>12</code></td><td><code>CODE_TIMEOUT</code></td><td>The Hub timed out while probing that IP.</td></tr>
<tr><td><code>13</code></td><td><code>CODE_REFUSED</code></td><td>The TCP connection was refused.</td></tr>
<tr><td><code>14</code></td><td><code>CODE_WRONG_CAMERA</code></td><td>A different Foscam camera was found at that IP than the one expected.</td></tr>
<tr><td><code>99</code></td><td><code>CODE_ERROR</code></td><td>Generic discovery or protocol error.</td></tr>
</tbody></table>

#### 5.1.31 `POST /hub/protocolconfig`

**Access:** `USER_ADMIN_ACCESS`

Sends protocol configuration commands to the matching protocol controller.

Request body:

- list containing one protocol configuration command

In practice, clients should send one command per request body.

Z-Wave commands sent through this endpoint are documented in chapter 8.

Modbus RTU manual pairing commands sent through this endpoint are documented in chapter 10.

Modbus TCP server commands sent through this endpoint are documented in chapter 11.

Example request:

```json
[
  {
    "type": "ZWAVE_START_INCLUSION"
  }
]
```

Example response when Z-Wave is not available:

```json
{
  "type": "ZWAVE_INCLUSION_STATUS",
  "status": "NO_ZWAVE",
  "nodeId": null
}
```

#### 5.1.32 `GET /hub/serialports`

**Access:** `USER_ADMIN_ACCESS`

Returns serial port configuration data.

This endpoint is the authoritative source for serial-port configuration choices.
Clients should read this response first and build the serial-port UI dynamically from the returned objects instead of assuming fixed controller types or fixed baud/data/stop/parity options.

In practice:

- each returned `SERIAL_CONFIG` object represents one configurable serial controller
- `controllerType` identifies which controller the settings apply to
- `allowedBaurates`, `allowedDatabits`, `allowedStopbits`, and `allowedParities` define the valid settings for that controller on that Hub build / feature set
- clients should only allow combinations derived from this response
- the example below is illustrative only; actual controllers and allowed values may differ

Example response:

```json
[
  {
    "type": "SERIAL_CONFIG",
    "controllerType": "MODBUS_CONTROLLER",
    "name": "Modbus",
    "description": "RS-485 port configuration",
    "allowedBaurates": [
      9600,
      19200,
      38400
    ],
    "allowedDatabits": [
      7,
      8
    ],
    "allowedStopbits": [
      1,
      2
    ],
    "allowedParities": [
      "none",
      "even",
      "odd"
    ],
    "settings": {
      "type": "SERIAL_SETTINGS",
      "baudrate": 19200,
      "databits": 8,
      "stopbits": 1,
      "parity": "none"
    }
  }
]
```

#### 5.1.33 `POST /hub/serialport`

**Access:** `USER_ADMIN_ACCESS`

Configures a serial port controller. On selected Hub feature sets, owner-level rights may be required.

Request body:

- serial configuration object

The request should be based on an object previously returned by `GET /hub/serialports`.
Clients should keep the returned `controllerType` and send `settings` using only values allowed for that specific controller.
Do not hardcode protocol-specific defaults from the documentation examples.

Example request:

```json
{
  "type": "SERIAL_CONFIG",
  "controllerType": "MODBUS_CONTROLLER",
  "settings": {
    "type": "SERIAL_SETTINGS",
    "baudrate": 19200,
    "databits": 8,
    "stopbits": 1,
    "parity": "none"
  }
}
```

#### 5.1.34 `GET /hub/sync`

**Access:** `USER_ADMIN_ACCESS`

Forces immediate state save/synchronization.

Example request:

```http
GET /cc/<api-version>/hub/sync
```

#### 5.1.35 `GET /hub/restart`

**Access:** `USER_ADMIN_ACCESS`

Restarts the Hub application.

Example request:

```http
GET /cc/<api-version>/hub/restart
```

#### 5.1.36 `GET /hub/reboot`

**Access:** `USER_ADMIN_ACCESS`

Reboots the Hub device.

Example request:

```http
GET /cc/<api-version>/hub/reboot
```

#### 5.1.37 `GET /hub/shutdown`

> `1.14.15+` only.

**Access:** `USER_ADMIN_ACCESS`

Shuts the Hub device down.

Example request:

```http
GET /cc/<api-version>/hub/shutdown
```

#### 5.1.38 `POST /hub/remotepair`

> `1.14.15+` only.

**Access:** `SUPER_ADMIN_ACCESS`

Creates a device directly from a serialized discovery-device description.

The request body must be a concrete `BaseDevice` subclass from the discovery data model. The Hub forwards the
message to `HUB_MANAGER` for asynchronous device creation.

Request body:

- concrete `BaseDevice` object

Response:

- no documented response body

Example notes:

- this is not a normal user pairing endpoint
- the request body must be a valid `BaseDevice` payload

#### 5.1.39 `PUT /hub/autoconfig`

**Access:** `USER_ADMIN_ACCESS`

Automatic configuration of the Hub.

This endpoint creates devices, rooms, scenes, and rules from configuration data.

If another autoconfiguration is already running, the new request is queued and processed after the earlier
configuration has finished.

Request body:

- `AutoConfiguration` object, documented in [11.1 Autoconfiguration data](#111-autoconfiguration-data)

Response:

- `true` on success, otherwise an error

Example request:

```json
{
  "type": "DC_AUTO_CONFIG",
  "name": "Reference configuration",
  "hubName": "Example Hub",
  "rooms": {
    "type": "DC_ROOM_CONFIG",
    "names": [
      "Living room",
      "Bedroom"
    ],
    "clear": false
  }
}
```

Example response:

```json
true
```

### 5.2 Device endpoints

This section documents the endpoints exposed under `/devices`.

Resource map:

- read object: [Device read object](#device-read-object)
- write commands: [Device command objects](#device-command-objects)
- configuration objects: [Device configuration objects](#device-configuration-objects)
- examples: [6.8 Device data](#68-device-data) and the endpoint examples in this section

#### 5.2.1 `GET /devices`

**Access:** `GUEST_ACCESS`

Fetches devices.

Request parameters:

- `groupId`: optional group identifier. If provided, only devices in that group are returned.

Response:

- dictionary `{device id: device status}`

The returned objects are defined in [Device read object](#device-read-object).

Example response:

```json
{
  "<device-uuid>": {
    "type": "LIGHT",
    "id": "<device-uuid>",
    "name": "Living room lamp",
    "capabilities": {
      "type": "SET",
      "values": [
        "ON_OFF",
        "BRIGHTNESS"
      ]
    },
    "room": [
      "<room-uuid>"
    ],
    "state": {
      "type": "STATE_LIGHT",
      "isOn": true,
      "brightness": 0.75
    }
  }
}
```

#### 5.2.2 `PUT /devices`

**Access:** `IDENTIFIED_USER_ACCESS`

Sets device metadata such as name and room.

Request body:

- list of device metadata commands

The request objects are defined in [Device command objects](#device-command-objects).

Additional access rules:

- `DeviceAccessCommand` requires owner-level access
- `DeviceVisibilityCommand` requires owner-level access
- `DeviceLockedCommand` requires owner-level access

Example request:

```json
[
  {
    "type": "CMD_DEVICE_META",
    "id": "<device-uuid>",
    "name": "Living room lamp",
    "room": [
      "<room-uuid>"
    ]
  }
]
```

#### 5.2.3 `DELETE /devices`

**Access:** `USER_ADMIN_ACCESS`

Deletes devices.

Request body:

- list of device identifiers

Example request:

```json
[
  "<device-uuid>"
]
```

#### 5.2.4 `PUT /devices/command`

**Access:** `GUEST_ACCESS`

Sends commands to devices.

This endpoint mixes asynchronous control commands and synchronous query-style commands:

- normal device control commands are dispatched asynchronously
- `IgnoreDeviceCmd` and `ResetIgnoreCmd` are synchronous and require `USER_ADMIN_ACCESS`
- selected query-style commands return immediate data from the device or Hub

For these two ignore-management commands, the serialized command types are:

- `IgnoreDeviceCmd` -> `CMD_IGNORE_DEVICE`
- `ResetIgnoreCmd` -> `CMD_RESET_IGNORED`

For a remote caller with `REMOTE_GUEST` role, only these command types are allowed:

- `GetCDCommand`
- `GetQueueCommand`
- `GetAirpatrollerPumps`
- `GetFnipConfigs`
- `GetUpgradeInfos`
- `GetUpgradeInfo`

Special cases:

- `GetUpgradeInfo` returns the upgrade information for the target device
- `GetUpgradeInfos` returns Hub-level upgrade information
- `SearchCommand`, `UseCredentials`, `ChangeCredentials`, `ScanWifis`, `SetWifi`, `FoscamHostAtIp`, `GetCDCommand`, `GetQueueCommand`, `GetAirpatrollerPumps`, and `GetFnipConfigs` return immediate results
- `SnapShotCmd` is dispatched without an immediate payload result
- `RefreshDevice` is dispatched asynchronously; updated camera or device metadata is observed later via poll data

The command payloads are defined in [Device command objects](#device-command-objects).

Example request:

```json
[
  {
    "type": "CMD_DEVICE_ON",
    "id": "<device-uuid>"
  }
]
```

#### 5.2.5 `PUT /devices/preset`

**Access:** `GUEST_ACCESS`

Runs device presets.

Request body:

- list of preset objects

Additional access rules:

- for remote calls, at least `IDENTIFIED_USER_ACCESS` is required

Each preset object is expected to contain at least:

- `targetIds`: target device ids
- `state`: device state to apply

The reused `Preset` object shape is defined in [Scene read objects](#scene-read-objects).

Example request:

```json
[
  {
    "type": "PRESET",
    "targetIds": [
      "<device-uuid>"
    ],
    "state": {
      "type": "STATE_LIGHT",
      "isOn": true,
      "brightness": 0.4
    }
  }
]
```

#### 5.2.6 `GET /devices/configuration`

**Access:** `USER_ADMIN_ACCESS`

Fetches device configuration.

Request parameters:

- `deviceId`: target device identifier
- `templates`: optional string flag, default `True`. When `true`, configuration templates are included.

Response:

- device configuration object

The returned object is defined in [Device configuration objects](#device-configuration-objects).

Example response:

```json
{
  "type": "CONFIGURATION",
  "id": "<device-uuid>",
  "template": {
    "type": "CFG_TEMPLATE",
    "parameters": [
      {
        "type": "CFG_ITEM",
        "name": "brightness_point_type",
        "fieldType": "OPTION",
        "description": "Brightness point object type",
        "help": "Select which BACnet object type is used for the brightness point.",
        "defaultValue": "analogOutput",
        "options": {
          "Analog Value": "analogValue",
          "Analog Input": "analogInput",
          "Analog Output": "analogOutput"
        },
        "readOnly": false
      },
      {
        "type": "CFG_ITEM",
        "name": "brightness_point_id",
        "fieldType": "NUMBER",
        "description": "Brightness point object id",
        "help": "Object instance number for the BACnet brightness point.",
        "readOnly": false
      }
    ]
  },
  "parameters": {
    "brightness_point_type": "analogOutput",
    "brightness_point_id": 7
  }
}
```

#### 5.2.7 `PUT /devices/configuration`

**Access:** `USER_ADMIN_ACCESS`

Saves device configuration.

Request body:

- device configuration object

The request object is defined in [Device configuration objects](#device-configuration-objects).

Response:

- `true` if the configuration was accepted

Example request:

```json
{
  "type": "CONFIGURATION",
  "id": "<device-uuid>",
  "parameters": {
    "brightness_point_type": "analogOutput",
    "brightness_point_id": 9
  }
}
```

Example response:

```json
true
```

#### 5.2.8 `DELETE /devices/configuration`

**Access:** `USER_ADMIN_ACCESS`

Removes device configuration and restores default values.

Request parameters:

- `deviceId`: target device identifier

Response:

- `true` if successful

Example request:

```http
DELETE /cc/<api-version>/devices/configuration?deviceId=<device-uuid>
```

Example response:

```json
true
```

### 5.3 Group endpoints

This section documents the endpoints exposed under `/groups`.

Resource map:

- read object: [Group read object](#group-read-object)
- write commands: [Group command objects](#group-command-objects)
- shared partial-state command: [Device command objects](#device-command-objects)
- examples: [6.9 Group data](#69-group-data) and the endpoint examples in this section

#### 5.3.1 `GET /groups`

**Access:** `GUEST_ACCESS`

Fetches all groups.

Response:

- dictionary `{group id: group data}`

The returned objects are defined in [Group read object](#group-read-object).

Example response:

```json
{
  "<group-uuid>": {
    "type": "GROUP",
    "id": "<group-uuid>",
    "name": "Living room lights",
    "members": [
      "<device-uuid>",
      "<device-uuid-2>"
    ],
    "room": [
      "<room-uuid>"
    ]
  }
}
```

#### 5.3.2 `PUT /groups`

**Access:** `IDENTIFIED_USER_ACCESS`

Creates or modifies groups.

To create a new group, set the group's `id` field to `null`.

Request body:

- list of group modification commands

The request objects are defined in [Group command objects](#group-command-objects).

Response:

- list of modified group objects

The returned objects are defined in [Group read object](#group-read-object).

Notes:

- the returned group objects do not include the `state` attribute

Example request:

```json
[
  {
    "type": "CMD_GROUP",
    "id": null,
    "name": "Living room lights",
    "members": [
      "<device-uuid>",
      "<device-uuid-2>"
    ],
    "room": [
      "<room-uuid>"
    ]
  }
]
```

Example response:

```json
[
  {
    "type": "GROUP",
    "id": "<group-uuid>",
    "name": "Living room lights",
    "members": [
      "<device-uuid>",
      "<device-uuid-2>"
    ],
    "room": [
      "<room-uuid>"
    ]
  }
]
```

#### 5.3.3 `PUT /groups/command`

**Access:** `GUEST_ACCESS`

Sends commands to groups.

Supported command types:

- `DeviceCommand`
- `GroupOnCommand`
- `GroupOffCommand`

Additional access rules:

- for remote calls, at least `IDENTIFIED_USER_ACCESS` is required

The group-specific command payloads are defined in [Group command objects](#group-command-objects). `PUT /groups/command` may also use the same `CMD_DEVICE` payload shape documented in [Device command objects](#device-command-objects), in which case the target `id` is the group id.

Response:

- list of command handling results returned by the group operation

Example request:

```json
[
  {
    "type": "CMD_GROUP_ON",
    "id": "<group-uuid>"
  }
]
```

#### 5.3.4 `DELETE /groups`

**Access:** `IDENTIFIED_USER_ACCESS`

Deletes a group.

Request parameters:

- `groupId`: target group identifier

Response:

- `true` if successful

Example request:

```http
DELETE /cc/<api-version>/groups?groupId=<group-uuid>
```

Example response:

```json
true
```

### 5.4 Rule endpoints

This section documents the endpoints exposed under `/rules`.

Resource map:

- configuration objects: [Rule configuration objects](#rule-configuration-objects)
- read objects: [Rule read objects](#rule-read-objects)
- write commands: [Rule command objects](#rule-command-objects)
- typed helper values: [Typed rule value objects](#typed-rule-value-objects)
- examples: [6.10 Rule data](#610-rule-data) and the endpoint examples in this section

#### 5.4.1 `GET /rules`

**Access:** `GUEST_ACCESS`

Fetches rules.

Request parameters:

- `ruleId`: optional rule identifier. If provided, only that rule is returned.

Response:

- dictionary `{rule id: rule data}`

The returned objects are defined in [Rule read objects](#rule-read-objects).

Example response:

```json
{
  "<rule-uuid>": {
    "type": "RULE",
    "id": "<rule-uuid>",
    "is_on": true,
    "scenes": {},
    "timers": [],
    "config": {
      "type": "CONFIG",
      "id": "<rule-uuid>",
      "name": "Hall auto light",
      "configType": "AUTO_LIGHT_RULE",
      "inputs": {
        "motions": [
          "<device-uuid>"
        ]
      },
      "outputs": {
        "lights": [
          "<device-uuid-2>"
        ]
      },
      "extras": {
        "timeout": [
          {
            "type": "TIME_DELTA_MS",
            "value": 300000
          }
        ]
      }
    },
    "timestamp": 1710249600000
  }
}
```

#### 5.4.2 `PUT /rules`

**Access:** `IDENTIFIED_USER_ACCESS`

Creates or updates rules.

Request body:

- `rules`: list of rule configurations
- `onConfiguration`: rule on-configuration applied together with the rule configuration

The request object shapes are defined in [Rule configuration objects](#rule-configuration-objects) and [Typed rule value objects](#typed-rule-value-objects). The response objects are defined in [Rule read objects](#rule-read-objects).

Response:

- list of created or updated rule objects

Example request:

```json
{
  "rules": [
    {
      "type": "CONFIG",
      "id": null,
      "name": "Hall auto light",
      "configType": "AUTO_LIGHT_RULE",
      "inputs": {
        "motions": [
          "<device-uuid>"
        ]
      },
      "outputs": {
        "lights": [
          "<device-uuid-2>"
        ]
      },
      "extras": {
        "timeout": [
          {
            "type": "TIME_DELTA_MS",
            "value": 300000
          }
        ]
      }
    }
  ],
  "onConfiguration": {
    "type": "RULE_ON_CFG",
    "scenes": [],
    "timers": []
  }
}
```

#### 5.4.3 `PUT /rules/command`

**Access:** `IDENTIFIED_USER_ACCESS`

Sends commands to rules.

Supported command types:

- `RuleOnCommand`
- `RuleOffCommand`

The command payloads are defined in [Rule command objects](#rule-command-objects), and the returned objects are defined in [Rule read objects](#rule-read-objects).

Response:

- list of rule data objects indicating the state after the command is handled

Example request:

```json
[
  {
    "type": "RULE_ON_COMMAND",
    "id": "<rule-uuid>"
  }
]
```

#### 5.4.4 `DELETE /rules`

**Access:** `IDENTIFIED_USER_ACCESS`

Deletes a rule.

Request parameters:

- `ruleId`: target rule identifier

Example request:

```http
DELETE /cc/<api-version>/rules?ruleId=<rule-uuid>
```

#### 5.4.5 `GET /rules/templates`

**Access:** `GUEST_ACCESS`

Returns available rule templates.

The returned template objects are defined in [Rule configuration objects](#rule-configuration-objects).

Example response:

```json
[
  {
    "type": "TEMPLATE",
    "configType": "AUTO_LIGHT_RULE",
    "name": "Auto light",
    "description": "Lights on when movement",
    "inputs": {
      "motions": {
        "type": "INPUT",
        "fieldType": "MOTION",
        "capabilities": [],
        "displayOrder": 1,
        "description": "Detects movement",
        "minCount": 1,
        "maxCount": 1000
      },
      "twilights": {
        "type": "INPUT",
        "fieldType": null,
        "capabilities": [
          "TWILIGHT"
        ],
        "displayOrder": 2,
        "description": "Twilight sensor",
        "minCount": 0
      }
    },
    "outputs": {
      "lights": {
        "type": "OUTPUT",
        "fieldType": "LIGHT",
        "capabilities": [
          "ON_OFF"
        ],
        "displayOrder": 3,
        "description": "Turn on",
        "minCount": 1
      }
    },
    "extras": {
      "timeout": {
        "type": "VALUE",
        "fieldType": "TIME_DELTA_MS",
        "displayOrder": 4,
        "description": "Turn off after no movement",
        "defaultValue": 300000,
        "minCount": 0,
        "maxCount": 1
      }
    }
  }
]
```

#### 5.4.6 `GET /rules/template`

**Access:** `USER_ADMIN_ACCESS`

Returns the stored source code of a single installed custom rule.

This endpoint is keyed by `ruleType`, but it reads only persisted custom-rule source code. It does not return the
implementation source of factory rules.

Request parameters:

- `ruleType`: target rule type

Example request:

```http
GET /cc/<api-version>/rules/template?ruleType=MY_CUSTOM_RULE
```

#### 5.4.7 `GET /rules/log`

**Access:** `USER_ADMIN_ACCESS`

Returns the log for a single custom rule type.

This endpoint is intended for custom rules and requires the Hub `CUSTOM_RULES` feature. Without that feature, the endpoint is not usable.

Request parameters:

- `ruleType`: target rule type

Example request:

```http
GET /cc/<api-version>/rules/log?ruleType=MY_CUSTOM_RULE
```

#### 5.4.8 `POST /rules/template`

**Access:** `USER_ADMIN_ACCESS`

Installs a custom rule.

This endpoint is intended for custom rules and requires the Hub `CUSTOM_RULES` feature. Without that feature, the endpoint is not usable.

Request parameters:

- `ruleLogic`: custom rule source code as a string

Example request:

```http
POST /cc/<api-version>/rules/template?ruleLogic=<python-rule-source>
```

#### 5.4.9 `DELETE /rules/template`

**Access:** `USER_ADMIN_ACCESS`

Uninstalls a custom rule.

This endpoint is intended for custom rules and requires the Hub `CUSTOM_RULES` feature. Without that feature, the endpoint is not usable.

Request parameters:

- `ruleType`: target rule type

Example request:

```http
DELETE /cc/<api-version>/rules/template?ruleType=MY_CUSTOM_RULE
```

#### 5.4.10 `PUT /rules/onConfiguration`

**Access:** `IDENTIFIED_USER_ACCESS`

Updates rule on-configuration.

Request parameters:

- `ruleId`: target rule identifier

Request body:

- on-configuration object

The request object is defined in [Rule configuration objects](#rule-configuration-objects).

Example request:

```json
{
  "type": "RULE_ON_CFG",
  "scenes": [
    "<scene-uuid>"
  ],
  "timers": []
}
```

#### 5.4.11 `POST /rules/proposals`

**Access:** `IDENTIFIED_USER_ACCESS`

Returns rule proposals.

Request body:

- list of rule proposal request commands

The returned proposal objects are defined in [Rule read objects](#rule-read-objects), with nested configuration objects defined in [Rule configuration objects](#rule-configuration-objects).

Example request:

```json
[
  {
    "type": "CMD_GET_PROPOSALS",
    "deviceIds": [
      "<device-uuid>"
    ],
    "room": "<room-uuid>",
    "maxNum": 5
  }
]
```

Example response:

```json
[
  {
    "type": "RULE_PROPOSAL",
    "config": {
      "type": "CONFIG",
      "id": null,
      "name": "Hall auto light",
      "configType": "AUTO_LIGHT_RULE",
      "inputs": {
        "motions": [
          "<device-uuid>"
        ]
      },
      "outputs": {
        "lights": [
          "<device-uuid-2>"
        ]
      },
      "extras": {
        "timeout": [
          {
            "type": "TIME_DELTA_MS",
            "value": 300000
          }
        ]
      }
    },
    "onConfig": {
      "type": "RULE_ON_CFG",
      "scenes": [],
      "timers": []
    }
  }
]
```

### 5.5 Scene endpoints

This section documents the endpoints exposed under `/scenes`.

Resource map:

- read objects: [Scene read objects](#scene-read-objects)
- write commands: [Scene command objects](#scene-command-objects)
- examples: [6.11 Scene data](#611-scene-data) and the endpoint examples in this section

#### 5.5.1 `GET /scenes`

**Access:** `GUEST_ACCESS`

Fetches scenes.

Request parameters:

- `sceneId`: optional scene identifier. If provided, only that scene is returned.

Response:

- dictionary `{scene id: scene data}`

The returned objects are defined in [Scene read objects](#scene-read-objects).

Example response:

```json
{
  "<scene-uuid>": {
    "type": "SCENE",
    "id": "<scene-uuid>",
    "name": "Movie",
    "category": "USER",
    "isOn": false,
    "presets": {},
    "excludedIds": [],
    "sceneTimes": []
  }
}
```

#### 5.5.2 `PUT /scenes`

**Access:** `IDENTIFIED_USER_ACCESS`

Creates or modifies scenes.

To create a new scene, set the scene `id` to `null`.

Request body:

- list of `ChangeScene` commands

The request objects are defined in [Scene command objects](#scene-command-objects).

Response:

- list of modified scene objects

The returned objects are defined in [Scene read objects](#scene-read-objects).

Example request:

```json
[
  {
    "type": "CMD_SCENE",
    "id": null,
    "name": "Movie",
    "isOn": false,
    "presets": {
      "<preset-uuid>": {
        "type": "PRESET",
        "targetIds": [
          "<device-uuid>"
        ],
        "state": {
          "type": "STATE_LIGHT",
          "isOn": true,
          "brightness": 0.2
        }
      }
    },
    "sceneTimes": []
  }
]
```

#### 5.5.3 `PUT /scenes/command`

**Access:** `GUEST_ACCESS`

Sends commands to scenes.

Supported command types:

- `ChangeScene`
- `SceneOnCommand`
- `SceneOffCommand`

Additional access rules:

- for remote calls, at least `IDENTIFIED_USER_ACCESS` is required
- `ChangeScene` itself requires `IDENTIFIED_USER_ACCESS` even on local calls

Response:

- list of scene data objects indicating the state after the command is handled

The command payloads are defined in [Scene command objects](#scene-command-objects), and the returned scene objects are defined in [Scene read objects](#scene-read-objects).

Example request:

```json
[
  {
    "type": "CMD_SCENE_ON",
    "id": "<scene-uuid>"
  }
]
```

#### 5.5.4 `DELETE /scenes`

**Access:** `IDENTIFIED_USER_ACCESS`

Deletes a scene.

Request parameters:

- `sceneId`: target scene identifier

Response:

- `true` if successful

Example request:

```http
DELETE /cc/<api-version>/scenes?sceneId=<scene-uuid>
```

Example response:

```json
true
```

### 5.6 Room endpoints

This section documents the endpoints exposed under `/rooms`.

Resource map:

- read objects: [Room read objects](#room-read-objects)
- write commands: [Room command objects](#room-command-objects)
- examples: [6.12 Room data](#612-room-data) and the endpoint examples in this section

#### 5.6.1 `GET /rooms`

**Access:** `GUEST_ACCESS`

Fetches rooms.

Request parameters:

- `roomId`: optional room identifier. If provided, only that room is returned.

Response:

- dictionary `{room id: room data}`

The returned objects are defined in [Room read objects](#room-read-objects).

Example response:

```json
{
  "<room-uuid>": {
    "type": "ROOM",
    "id": "<room-uuid>",
    "name": "Living room",
    "order": 0,
    "status": {
      "type": "ROOM_STATUS",
      "id": "<room-uuid>"
    }
  }
}
```

#### 5.6.2 `PUT /rooms`

**Access:** `IDENTIFIED_USER_ACCESS`

Creates or modifies rooms.

To create a new room, set the room `id` to `null`.

Request body:

- list of room commands

The request objects are defined in [Room command objects](#room-command-objects).

Response:

- list of success values

Example request:

```json
[
  {
    "type": "CMD_ROOM",
    "id": null,
    "name": "Living room"
  }
]
```

Example response:

```json
[
  true
]
```

#### 5.6.3 `PUT /rooms/order`

**Access:** `IDENTIFIED_USER_ACCESS`

Changes room ordering.

Request body:

- list of `SetRoomOrder` commands

The request objects are defined in [Room command objects](#room-command-objects).

Example request:

```json
[
  {
    "type": "CMD_SET_ROOM_ORDER",
    "id": "<room-uuid>",
    "order": 0
  }
]
```

#### 5.6.4 `DELETE /rooms`

**Access:** `IDENTIFIED_USER_ACCESS`

Deletes a room.

Request parameters:

- `roomId`: target room identifier

Response:

- `true` if successful

Example request:

```http
DELETE /cc/<api-version>/rooms?roomId=<room-uuid>
```

Example response:

```json
true
```

### 5.7 Alarm endpoints

This section documents the endpoints exposed under `/alarms`.

#### 5.7.1 `GET /alarms`

**Access:** `GUEST_ACCESS`

Fetches alarms.

Request parameters:

- `alarmId`: optional alarm identifier. If provided, only that alarm is returned.

Response:

- dictionary `{alarm id: alarm data}`

Example response:

```json
{
  "<alarm-uuid>": {
    "type": "USER_ALARM",
    "id": "<alarm-uuid>",
    "sourceId": "<device-uuid>",
    "title": {
      "": "Leak alarm"
    },
    "name": "Water leak",
    "message": {
      "": "<p>Leak detected in utility room.</p>"
    },
    "level": "err",
    "closed": false,
    "timestamp": 1710249600000,
    "createdAtMs": 1710249600000
  }
}
```

#### 5.7.2 `PUT /alarms/close`

**Access:** `IDENTIFIED_USER_ACCESS`

Closes an alarm.

Request parameters:

- `alarmId`: target alarm identifier

Response:

- alarm close result

Example request:

```http
PUT /cc/<api-version>/alarms/close?alarmId=<alarm-uuid>
```

#### 5.7.3 `DELETE /alarms`

**Access:** `IDENTIFIED_USER_ACCESS`

Deletes an alarm.

Request parameters:

- `alarmId`: target alarm identifier

Response:

- alarm deletion result

Example request:

```http
DELETE /cc/<api-version>/alarms?alarmId=<alarm-uuid>
```

### 5.8 BACnet endpoints

This section documents the endpoints exposed under `/bacnet`.

All BACnet endpoints require `USER_ADMIN_ACCESS`.

#### 5.8.1 `GET /bacnet/settings`

Returns the current BACnet server settings.

Notes:

- these settings affect the Hub's BACnet server functionality only
- BACnet client-side discovery and pairing are not configured through this object
- `bacnetEnabled` reflects whether BACnet server functionality is enabled

Example response:

```json
{
  "type": "BACNET_SETTINGS",
  "bacnetEnabled": true,
  "deviceId": 12345,
  "deviceName": "Example Hub",
  "port": 47808,
  "broadcast": true
}
```

#### 5.8.2 `POST /bacnet/settings`

Saves BACnet server settings.

Request body:

- BACnet settings object

Validation rules:

- `deviceId` must be an integer in the valid BACnet device-id range used by the implementation
- `deviceName` must be a non-empty string
- `port` must be an integer greater than or equal to `1025`
- `broadcast` must be boolean when provided

Response:

- `true` on success

Example request:

```json
{
  "type": "BACNET_SETTINGS",
  "bacnetEnabled": true,
  "deviceId": 12345,
  "deviceName": "Example Hub",
  "port": 47808,
  "broadcast": true
}
```

#### 5.8.3 `POST /bacnet/defaultsettings`

Restores default BACnet settings.

Behavior:

- resets `port` to `47808`
- resets `broadcast` to `true`
- regenerates the Hub BACnet `deviceId` and `deviceName` from the Hub identity

Response:

- `true` on success

Example request:

```http
POST /cc/<api-version>/bacnet/defaultsettings
```

#### 5.8.4 `GET /bacnet/localdevicetypes`

Returns the available local BACnet device types.

Example response:

```json
[
  {
    "type": "BACNET_LOCAL_PAIRING_INFO",
    "id": "bacnetSignal",
    "objectType": "binaryValue",
    "name": "Signal",
    "capability": "SIGNAL"
  }
]
```

#### 5.8.5 `GET /bacnet/objecttypes`

Returns the available BACnet object types.

Example response:

```json
{
  "analogInput": "Analog input",
  "analogValue": "Analog value",
  "binaryInput": "Binary input",
  "binaryValue": "Binary value"
}
```

#### 5.8.6 `POST /bacnet/device`

Creates a local BACnet device or object.

Request body:

- local BACnet device creation request

Notes:

- this endpoint requires BACnet pairing to be active
- this endpoint requires BACnet server functionality to be enabled
- the request model contains `objectId` and `objectName`, but they are currently not used by the implementation

Example request:

```json
{
  "type": "CREATE_BACNET_DEVICE",
  "id": "bacnetSignal",
  "objectId": 1001,
  "objectName": "Signal object"
}
```

Example response:

```json
true
```

#### 5.8.7 `POST /bacnet/scan`

Scans BACnet devices.

Request body:

- BACnet scan request

Example request:

```json
{
  "type": "SCAN_BACNET_DEVICE",
  "address": "192.168.1.60:47808",
  "deviceId": 1234,
  "objectType": "binaryInput",
  "objectId": 1
}
```

#### 5.8.8 `GET /bacnet/devices`

Returns available BACnet devices discovered by scanning.

Example response:

```json
{
  "1234:device:1234": {
    "type": "BACNET_HOST",
    "identifier": "1234:device:1234",
    "objectId": 1234,
    "objectType": "device",
    "address": "192.168.1.60:47808",
    "deviceId": 1234,
    "objectName": "AHU-1",
    "vendorName": "Example Vendor",
    "modelName": "Example Model",
    "alreadyPaired": false,
    "possibleDevices": [
      "ON_OFF",
      "SIGNAL"
    ],
    "deviceObjects": {}
  }
}
```

#### 5.8.9 `POST /bacnet/pair`

Pairs a BACnet object into the Hub.

Request body:

- BACnet pairing request

Notes:

- this endpoint is intended to be used while BACnet pairing is active

Example request:

```json
{
  "type": "PAIR_BACNET_DEVICE",
  "deviceId": 1234,
  "objectType": "binaryInput",
  "objectId": 1,
  "capability": "SIGNAL"
}
```

Example response:

```json
true
```

### 5.9 Log endpoint

This section documents the log endpoint exposed by the local HTTP handler.

#### 5.9.1 `GET /log`

Streams the local `hub.log` file over HTTP under the versioned API prefix.

Notes:

- this endpoint is available only when the `LOG_SERVICE` Hub feature is enabled
- on the local network, the endpoint is anonymously accessible when the feature is enabled
- the response is a streaming text response
- if the logging feature is disabled, the endpoint returns `403`

Example request:

```http
GET /cc/<api-version>/log
```

## 6. API data structures

This chapter documents the shared data structures used by the Hub API. The goal is to explain what kind of objects the client receives and sends, how those objects are organized, and which fields are important for using the API correctly.

### 6.1 Hub feature codes

Hub features are represented as integer codes. They are returned by:

- `GET /hub` in the `features` field
- `GET /hub/features`

These codes are not display strings. A client that wants to show feature names should map the integer codes to their meanings.

General Hub feature codes:

<table>
<thead><tr><th>Code</th><th>Name</th><th>Explanation</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>1</code> (<code>0x01</code>)</td><td><code>VIDEO_SERVICE</code></td><td>Camera and video service support.</td><td><code>1</code></td></tr>
<tr><td><code>2</code> (<code>0x02</code>)</td><td><code>CUSTOM_RULES</code></td><td>Custom rule support.</td><td><code>2</code></td></tr>
<tr><td><code>3</code> (<code>0x03</code>)</td><td><code>EVENTS</code></td><td>Hub can produce events to the cloud.</td><td><code>3</code></td></tr>
<tr><td><code>4</code> (<code>0x04</code>)</td><td><code>NO_NMAP_SCAN</code></td><td>LAN scanning with <code>nmap</code> is disabled.</td><td><code>4</code></td></tr>
<tr><td><code>5</code> (<code>0x05</code>)</td><td><code>REMOTE_USAGE</code></td><td>Remote Hub usage is enabled.</td><td><code>5</code></td></tr>
<tr><td><code>6</code> (<code>0x06</code>)</td><td><code>SMS_MESSAGING</code></td><td>Hub may send SMS messages.</td><td><code>6</code></td></tr>
<tr><td><code>7</code> (<code>0x07</code>)</td><td><code>PN_MESSAGING</code></td><td>Hub may send push notifications.</td><td><code>7</code></td></tr>
<tr><td><code>8</code> (<code>0x08</code>)</td><td><code>SEND_EMAIL</code></td><td>Hub may send emails.</td><td><code>8</code></td></tr>
<tr><td><code>9</code> (<code>0x09</code>)</td><td><code>SERVER_CONNECTION</code></td><td>Cloud connection monitoring.</td><td><code>9</code></td></tr>
<tr><td><code>10</code> (<code>0x0A</code>)</td><td><code>BACNET</code></td><td>Full BACnet integration.</td><td><code>10</code></td></tr>
<tr><td><code>11</code> (<code>0x0B</code>)</td><td><code>VIEW_HISTORY</code></td><td>History data viewing.</td><td><code>11</code></td></tr>
<tr><td><code>12</code> (<code>0x0C</code>)</td><td><code>ZWAVE</code></td><td>Z-Wave integration.</td><td><code>12</code></td></tr>
<tr><td><code>13</code> (<code>0x0D</code>)</td><td><code>REMOTE_SENSOR</code></td><td>Remote sensor pairing.</td><td><code>13</code></td></tr>
<tr><td><code>14</code> (<code>0x0E</code>)</td><td><code>LOG_SERVICE</code></td><td>Local HTTP log streaming.</td><td><code>14</code></td></tr>
<tr><td><code>15</code> (<code>0x0F</code>)</td><td><code>MODBUS</code></td><td>Modbus RTU integration.</td><td><code>15</code></td></tr>
<tr><td><code>16</code> (<code>0x10</code>)</td><td><code>EIGHT68</code></td><td>868 MHz integration.</td><td><code>16</code></td></tr>
<tr><td><code>17</code> (<code>0x11</code>)</td><td><code>MQTT</code></td><td>MQTT integration.</td><td><code>17</code></td></tr>
<tr><td><code>18</code> (<code>0x12</code>)</td><td><code>CELLULAR</code></td><td>Cellular modem support.</td><td><code>18</code></td></tr>
<tr><td><code>19</code> (<code>0x13</code>)</td><td><code>MBUS</code></td><td>M-Bus integration.</td><td><code>19</code></td></tr>
<tr><td><code>20</code> (<code>0x14</code>)</td><td><code>SPOT_PRICES</code></td><td>Electricity spot-price support.</td><td><code>20</code></td></tr>
<tr><td><code>21</code> (<code>0x15</code>)</td><td><code>PREMIUM</code></td><td>Premium subscription feature.</td><td><code>21</code></td></tr>
<tr><td><code>22</code> (<code>0x16</code>)</td><td><code>FOUR33_HW2</code></td><td>433 MHz integration on hardware generation 2.</td><td><code>22</code></td></tr>
<tr><td><code>23</code> (<code>0x17</code>)</td><td><code>BACNET_SERVER</code></td><td>BACnet server-only integration.</td><td><code>23</code></td></tr>
<tr><td><code>24</code> (<code>0x18</code>)</td><td><code>BACNET_CLIENT</code></td><td>BACnet client-only integration.</td><td><code>24</code></td></tr>
<tr><td><code>25</code> (<code>0x19</code>)</td><td><code>MODBUS_TCP_SERVER</code></td><td>Modbus TCP server.</td><td><code>25</code></td></tr>
<tr><td><code>32</code> (<code>0x20</code>)</td><td><code>BLE</code></td><td>Bluetooth LE.</td><td><code>32</code></td></tr>
</tbody></table>

The feature list is sorted by the Hub before it is returned.

### 6.2 Common representation patterns

Several data-structure conventions repeat throughout the API:

<table>
<thead><tr><th>Convention</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Most persisted objects contain a stable Hub-local identifier.</td><td>string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>type</code></td><td>Most objects contain a serialized object type marker. This discriminator is used by the Cozy de/serialization layer to instantiate the correct concrete class when data is loaded back from JSON.</td><td>string</td><td><code>POWER_SOCKET</code></td></tr>
<tr><td>Timestamps</td><td>Timestamps are milliseconds since Unix epoch unless documented otherwise.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td>Dictionary collections</td><td>Collection responses are often dictionaries keyed by object id instead of arrays.</td><td>object</td><td><code>{"&lt;device-uuid&gt;":{"type":"POWER_SOCKET"}}</code></td></tr>
<tr><td>Delta deletion</td><td>In delta dictionaries, a deleted object is represented by <code>null</code> for that object id unless the response is a full reload.</td><td>null value in object map</td><td><code>{"&lt;device-uuid&gt;":null}</code></td></tr>
<tr><td>Command lists</td><td>Many update endpoints accept lists of command objects even when only one command is sent.</td><td>array</td><td><code>[{"type":"CMD_DEVICE",...}]</code></td></tr>
<tr><td>Cozy set</td><td>Set-valued fields are serialized as Cozy <code>SET</code> objects.</td><td>object</td><td><code>{"type":"SET","values":["DEVICE","ON_OFF"]}</code></td></tr>
</tbody></table>

There are also two important collection styles:

- full-object dictionaries, such as `{device id: device status}`
- delta containers, which add a response timestamp and contain only objects changed after a given timestamp

Whenever this chapter documents a concrete data structure, the documented `type` value is the exact serialized value that should appear in payloads for that structure.

### 6.3 Hub metadata and lifecycle data

The Hub metadata object returned by `GET /hub` is `HubMetaData`.

```python
class HubMetaData(PollCmd):
    type = "HUB_META_DATA"

    def __init__(self):
        super().__init__()
        self.hubId = None
        self.name = None
        self.version = None
        self.state = None
        self.connected = None
        self.features = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>HUB_META_DATA</code></td></tr>
<tr><td><code>hubId</code></td><td>Globally unique Hub identifier.</td><td>UUID string</td><td><code>94f8c0a2-3b9a-4d4f-b65a-7e9b61c2a110</code></td></tr>
<tr><td><code>name</code></td><td>Human-readable Hub name.</td><td>string</td><td><code>Home Hub</code></td></tr>
<tr><td><code>version</code></td><td>Hub software version.</td><td>string</td><td><code>1.14.12.20</code></td></tr>
<tr><td><code>state</code></td><td>Lifecycle state of the Hub.</td><td>string</td><td><code>claimed</code></td></tr>
<tr><td><code>connected</code></td><td>Whether the Hub is currently connected to Cozify cloud services.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>features</code></td><td>Sorted list of enabled Hub feature codes.</td><td>array of integers</td><td><code>[10, 23, 32]</code></td></tr>
</tbody></table>

The lifecycle state is especially important during bootstrap:

- `factory_new` means the Hub has not yet been claimed
- `claimed` means the Hub already has an owner and normal authenticated API access applies

The effective access level of a caller is represented separately by role-related objects such as HubKeys and `UserInfo`, not by `HubMetaData`.

Example:

```json
{
  "type": "HUB_META_DATA",
  "hubId": "b40a33b4-23b4-14e5-925a-78c90baba039",
  "name": "Home Hub",
  "version": "1.14.12.20",
  "state": "claimed",
  "connected": true,
  "features": [10, 23, 32]
}
```

### 6.4 Poll and delta data

`GET /hub/poll` returns a single `Delta` object.

```python
class Delta(Message):
    type = "POLL_DELTA"

    def __init__(self):
        super().__init__()
        self.timestamp = None
        self.full = None
        self.polls = []
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>POLL_DELTA</code></td></tr>
<tr><td><code>timestamp</code></td><td>Timestamp of the delta response.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>full</code></td><td>Tells whether the Hub is forcing a full reload.</td><td>boolean</td><td><code>false</code></td></tr>
<tr><td><code>polls</code></td><td>List of individual delta objects.</td><td>array</td><td><code>[{"type":"DEVICE_DELTA"}, {"type":"ROOM_DELTA"}]</code></td></tr>
</tbody></table>

If `full` is `true`, the client should treat the response as a reload of the documented data categories in that response. In that case, deleted items are not represented separately.

The documented delta object types are:

<table>
<thead><tr><th>Delta object</th><th><code>type</code> value</th><th>Payload field</th><th>Value shape</th><th>Nested object</th></tr></thead>
<tbody>
<tr><td><code>DeviceDelta</code></td><td><code>DEVICE_DELTA</code></td><td><code>devices</code></td><td>Dictionary from device id to device status object or <code>null</code></td><td>Device status object, see sections 6.7 and 6.8</td></tr>
<tr><td><code>GroupDelta</code></td><td><code>GROUP_DELTA</code></td><td><code>groups</code></td><td>Dictionary from group id to <code>Group</code> or <code>null</code></td><td><code>Group</code>, see section 6.9</td></tr>
<tr><td><code>SceneDelta</code></td><td><code>SCENE_DELTA</code></td><td><code>scenes</code></td><td>Dictionary from scene id to <code>Scene</code> or <code>null</code></td><td><code>Scene</code>, see section 6.11</td></tr>
<tr><td><code>RuleDelta</code></td><td><code>RULE_DELTA</code></td><td><code>rules</code></td><td>Dictionary from rule id to <code>RuleData</code> or <code>null</code></td><td><code>RuleData</code>, see section 6.10</td></tr>
<tr><td><code>UserDelta</code></td><td><code>USER_DELTA</code></td><td><code>users</code></td><td>Dictionary from user id to <code>HubUser</code> or <code>null</code></td><td><code>HubUser</code>, see section 6.5</td></tr>
<tr><td><code>UserAlerts</code></td><td><code>USER_ALERTS</code></td><td><code>alerts</code></td><td>Dictionary from alert id to <code>UserAlert</code> or <code>null</code></td><td><code>UserAlert</code>, see section 6.13</td></tr>
<tr><td><code>RoomDelta</code></td><td><code>ROOM_DELTA</code></td><td><code>rooms</code></td><td>Dictionary from room id to <code>Room</code> or <code>null</code></td><td><code>Room</code>, see section 6.12</td></tr>
<tr><td><code>ZoneDelta</code></td><td><code>ZONE_DELTA</code></td><td><code>zones</code></td><td>Dictionary from zone id to <code>Zone</code> or <code>null</code></td><td><code>Zone</code> object with <code>type</code>, <code>id</code>, <code>name</code>, and <code>timestamp</code></td></tr>
<tr><td><code>AlarmDelta</code></td><td><code>ALARM_DELTA</code></td><td><code>alarms</code></td><td>Dictionary from alarm id to <code>UserAlarm</code> or <code>null</code></td><td><code>UserAlarm</code>, see section 6.13</td></tr>
<tr><td><code>ActivatorDelta</code></td><td><code>ACTIVATOR_DELTA</code></td><td><code>activators</code></td><td>Dictionary from activator id to <code>Activator</code> or <code>null</code></td><td><code>Activator</code> object with <code>type</code>, <code>id</code>, <code>name</code>, <code>presets</code>, <code>activatedAt</code>, and <code>timestamp</code></td></tr>
</tbody></table>

For the dictionaries inside those deltas:

- the key is the object id
- the value is the current object
- if an object was deleted after the client timestamp, the value is `null`
- if the top-level poll response has `full=true`, the returned dictionaries should be treated as complete replacements for the included categories

This is the main synchronization model used by the Hub API.

Examples of individual delta objects:

```json
[
  {
    "type": "DEVICE_DELTA",
    "timestamp": 1710249600000,
    "devices": {
      "<device-uuid>": {
        "type": "POWER_SOCKET",
        "id": "<device-uuid>",
        "name": "Smart Plug",
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "GROUP_DELTA",
    "timestamp": 1710249600000,
    "groups": {
      "<group-uuid>": {
        "type": "GROUP",
        "id": "<group-uuid>",
        "name": "Downstairs lights",
        "members": [
          "<device-uuid>"
        ],
        "room": [
          "<room-uuid>"
        ],
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "SCENE_DELTA",
    "timestamp": 1710249600000,
    "scenes": {
      "<scene-uuid>": {
        "type": "SCENE",
        "id": "<scene-uuid>",
        "category": "USER",
        "name": "Evening",
        "isOn": false,
        "presets": {},
        "excludedIds": [],
        "sceneTimes": [],
        "requiredIds": [],
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "RULE_DELTA",
    "timestamp": 1710249600000,
    "rules": {
      "<rule-uuid>": {
        "type": "RULE",
        "id": "<rule-uuid>",
        "is_on": false,
        "scenes": {},
        "timers": [],
        "config": {
          "type": "CONFIG",
          "id": "<rule-uuid>",
          "name": "Turn lights on when home scene is active",
          "configType": "SCENE_MAX_TIME_ON_RULE",
          "inputs": {
            "scenes": [
              "<scene-uuid>"
            ]
          },
          "outputs": {},
          "extras": {
            "timeout": [
              {
                "type": "TIME_DELTA_MS",
                "value": 300000
              }
            ]
          }
        },
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "USER_DELTA",
    "timestamp": 1710249600000,
    "users": {
      "<user-uuid>": {
        "type": "HUB_USER",
        "id": "<user-uuid>",
        "revoked": false,
        "connected": false,
        "timestamp": 1710249599000,
        "user_info": {
          "type": "USER_INFO",
          "role": 32,
          "user": {
            "type": "USER",
            "uid": "<user-uuid>",
            "email": "user@example.com",
            "nickname": "Alex",
            "phone": null,
            "defaults": null
          }
        }
      }
    }
  },
  {
    "type": "USER_ALERTS",
    "timestamp": 1710249600000,
    "alerts": {
      "<alert-uuid>": {
        "type": "USER_ALERT",
        "id": "<alert-uuid>",
        "msg_id": "RULE_DISABLED",
        "sourceId": "<rule-uuid>",
        "message": "Rule has been disabled.",
        "realtime_ms": 1710249599000,
        "error": false,
        "userId": null
      }
    }
  },
  {
    "type": "ROOM_DELTA",
    "timestamp": 1710249600000,
    "rooms": {
      "<room-uuid>": {
        "type": "ROOM",
        "id": "<room-uuid>",
        "name": "Living room",
        "order": 0,
        "status": {
          "type": "ROOM_STATUS",
          "id": "<room-uuid>"
        },
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "ZONE_DELTA",
    "timestamp": 1710249600000,
    "zones": {
      "<zone-uuid>": {
        "type": "ZONE",
        "id": "<zone-uuid>",
        "name": "Upstairs",
        "timestamp": 1710249599000
      }
    }
  },
  {
    "type": "ALARM_DELTA",
    "timestamp": 1710249600000,
    "alarms": {
      "<alarm-uuid>": {
        "type": "USER_ALARM",
        "id": "<alarm-uuid>",
        "sourceId": "<device-uuid>",
        "title": {
          "": "Temperature alarm"
        },
        "name": "Temperature alarm",
        "message": {
          "": "<div>Temperature too high</div>"
        },
        "timestamp": 1710249599000,
        "createdAtMs": 1710249598000,
        "level": "err",
        "closed": false
      }
    }
  },
  {
    "type": "ACTIVATOR_DELTA",
    "timestamp": 1710249600000,
    "activators": {
      "<activator-uuid>": {
        "type": "ACTIVATOR",
        "id": "<activator-uuid>",
        "name": "All lights on",
        "presets": {},
        "activatedAt": 1710249595000,
        "timestamp": 1710249599000
      }
    }
  }
]
```

Example of a deleted object in a delta:

```json
{
  "type": "RULE_DELTA",
  "timestamp": 1710249600000,
  "rules": {
    "<rule-uuid>": null
  }
}
```

Example top-level poll response containing multiple delta objects:

```json
{
  "type": "POLL_DELTA",
  "timestamp": 1710249600000,
  "full": false,
  "polls": [
    {
      "type": "DEVICE_DELTA",
      "timestamp": 1710249600000,
      "devices": {
        "<device-uuid>": {
          "type": "POWER_SOCKET",
          "id": "<device-uuid>",
          "name": "Smart Plug",
          "timestamp": 1710249599000
        }
      }
    },
    {
      "type": "ROOM_DELTA",
      "timestamp": 1710249600000,
      "rooms": {
        "<room-uuid>": {
          "type": "ROOM",
          "id": "<room-uuid>",
          "name": "Living room",
          "order": 0,
          "timestamp": 1710249598000
        }
      }
    }
  ]
}
```

### 6.5 User, role, and Hub-user data

The user-related objects exposed through `/hub/users` and user deltas are based on three nested structures:

- `User`
- `UserInfo`
- `HubUser`

```python
class User(object):
    type = "USER"

    def __init__(self):
        super().__init__()
        self.uid = None
        self.email = None
        self.nickname = None
        self.phone = None
        self.defaults = None
```

```python
class UserInfo(metaclass=CozyBase):
    type = "USER_INFO"

    def __init__(self, role=None, user=None):
        self.user = user
        self.role = role
```

```python
class HubUser(IdBase):
    type = "HUB_USER"

    def __init__(self, user_info=None, revoked=None, connected=None, timestamp=None):
        super().__init__(user_info.user.uid if user_info is not None else None)
        self.user_info = user_info
        self.revoked = revoked
        self.connected = connected
        self.timestamp = timestamp
```

`User` contains the human identity fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>USER</code></td></tr>
<tr><td><code>uid</code></td><td>User identifier.</td><td>UUID string</td><td><code>2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401</code></td></tr>
<tr><td><code>email</code></td><td>Email address.</td><td>string</td><td><code>developer@example.com</code></td></tr>
<tr><td><code>nickname</code></td><td>Nickname.</td><td>string</td><td><code>Developer</code></td></tr>
<tr><td><code>phone</code></td><td>Phone number.</td><td>string or null</td><td><code>null</code></td></tr>
<tr><td><code>defaults</code></td><td>Whether the user has saved profile data at least once.</td><td>boolean</td><td><code>true</code></td></tr>
</tbody></table>

`UserInfo` combines:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>USER_INFO</code></td></tr>
<tr><td><code>user</code></td><td>Nested <code>User</code> object.</td><td>object</td><td><code>{"type":"USER",...}</code></td></tr>
<tr><td><code>role</code></td><td>Numeric Hub role.</td><td>integer</td><td><code>64</code></td></tr>
</tbody></table>

`HubUser` is the object returned by the Hub for a user on a specific Hub.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>HUB_USER</code></td></tr>
<tr><td><code>id</code></td><td>Same as the user id.</td><td>UUID string</td><td><code>2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401</code></td></tr>
<tr><td><code>user_info</code></td><td>Nested <code>UserInfo</code> object.</td><td>object</td><td><code>{"type":"USER_INFO",...}</code></td></tr>
<tr><td><code>revoked</code></td><td>Whether the user's access has been revoked.</td><td>boolean</td><td><code>false</code></td></tr>
<tr><td><code>connected</code></td><td>Whether the user currently has an active session on the Hub.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>timestamp</code></td><td>Last update timestamp.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
</tbody></table>

In practice:

- `User` describes the person
- `UserInfo` describes the person plus Hub role
- `HubUser` describes the current Hub-specific access state of that person

Example:

```json
{
  "type": "HUB_USER",
  "id": "2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401",
  "user_info": {
    "type": "USER_INFO",
    "user": {
      "type": "USER",
      "uid": "2b8d7f5e-3d91-4a12-96ee-3df8f9a9b401",
      "email": "developer@example.com",
      "nickname": "Developer",
      "phone": null,
      "defaults": true
    },
    "role": 64
  },
  "revoked": false,
  "connected": true,
  "timestamp": 1710249600000
}
```

### 6.6 Scan and pairing data

The device pairing flow uses these shared data objects:

- `ScanDelta`
- `DeviceScan`
- `SetScanResult`

```python
class ScanDelta(Message):
    type = "SCAN_DELTA"

    def __init__(self):
        super().__init__()
        self.timestamp = None
        self.full = None
        self.devices = []
```

```python
class DeviceScan(IdBase):
    type = "DEVICE_SCAN"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.ignored = None
        self.info = None
        self.actionRequired = None
        self.pairingStatus = None
        self.status = None
        self.timestamp = None
```

```python
class SetScanResult(IdBase, Message):
    type = "SET_SCAN_RESULT"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.ignored = None
```

`ScanDelta` contains:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>SCAN_DELTA</code></td></tr>
<tr><td><code>timestamp</code></td><td>Scan timestamp.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>full</code></td><td>Whether the response is a full scan snapshot.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>devices</code></td><td>List of <code>DeviceScan</code> objects.</td><td>array</td><td><code>[{"type":"DEVICE_SCAN",...}]</code></td></tr>
</tbody></table>

`DeviceScan` describes one discovered candidate device:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>DEVICE_SCAN</code></td></tr>
<tr><td><code>id</code></td><td>Discovered device id.</td><td>string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>ignored</code></td><td>Whether the device is currently marked to be ignored.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>info</code></td><td>Optional user-facing pairing help text.</td><td>string or null</td><td><code>Press pairing button on the device</code></td></tr>
<tr><td><code>actionRequired</code></td><td>Whether user action is still needed.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>pairingStatus</code></td><td>Current pairing status. Allowed values are <code>CONNECTING</code>, <code>CONNECTED</code>, and <code>COMPLETE</code>.</td><td>string enum</td><td><code>CONNECTED</code></td></tr>
<tr><td><code>status</code></td><td>Current device status object if available.</td><td>object or null</td><td><code>{"type":"POWER_SOCKET",...}</code></td></tr>
<tr><td><code>timestamp</code></td><td>Last update timestamp.</td><td>timestampms</td><td><code>1710249599500</code></td></tr>
</tbody></table>

`DeviceScan.pairingStatus` uses these values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>CONNECTING</code></td><td>The Hub has discovered the device and pairing is still in progress.</td></tr>
<tr><td><code>CONNECTED</code></td><td>The device is connected to the pairing flow and is currently available as a discovered candidate.</td></tr>
<tr><td><code>COMPLETE</code></td><td>The pairing process for that device has completed.</td></tr>
</tbody></table>

When the Hub has no more specific pairing-status event for a discovered device, the current implementation defaults the scan-side value to `CONNECTED`.

`SetScanResult` is the write-side command used by `PUT /hub/scan`.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this command object.</td><td>string</td><td><code>SET_SCAN_RESULT</code></td></tr>
<tr><td><code>id</code></td><td>Target discovered device id.</td><td>string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>ignored</code></td><td>The desired ignored state.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

The most important practical rule is that newly discovered devices are ignored by default, so clients usually only need to set `ignored=false` for the devices selected for pairing.

Example:

```json
{
  "type": "SCAN_DELTA",
  "timestamp": 1710249600000,
  "full": true,
  "devices": [
    {
      "type": "DEVICE_SCAN",
      "id": "<device-uuid>",
      "ignored": true,
      "info": "Press pairing button on the device",
      "actionRequired": true,
      "pairingStatus": "CONNECTED",
      "status": {
        "type": "POWER_SOCKET",
        "id": "<device-uuid>",
        "name": "New smart plug"
      },
      "timestamp": 1710249599500
    }
  ]
}
```

### 6.7 Device capabilities

Device capabilities describe what a device can do, what measurements or state it can expose, and which commands make sense for that device. Capabilities are not inferred from the device type name alone. Clients should read the capability set of each device and build their UI and command payloads from that set.

Capabilities are accessed through `status.capabilities`. In JSON, this is serialized as a Cozy set object:

```json
{
  "type": "SET",
  "values": ["DEVICE", "ON_OFF", "CONTROL_LIGHT", "BRIGHTNESS", "COLOR_HS"]
}
```

In practice, the important data is the string list in `status.capabilities.values`.

Capability handling rules:

- If a capability is present, the client may assume that the corresponding attribute family or command family is supported.
- If a capability is missing, the client should not assume the attribute exists or that the related command will work.
- Several capabilities usually work together. For example, a color light typically has `CONTROL_LIGHT`, `ON_OFF`, and `COLOR_HS`, `COLOR_TEMP`, or both, often together with `BRIGHTNESS`.
- Some capabilities add concrete state attributes such as `state.temperature` or `state.motion`.
- Some capabilities are markers for protocol, device class, or optional APIs and do not add a dedicated state attribute of their own.
- When a capability definition points outside `state`, this is called out explicitly in the table below.

The capability name column uses the exact strings that appear in `status.capabilities`.

<table>
<thead><tr><th>Capability</th><th>Capability description</th><th>Attribute identifier</th><th>Attribute description</th><th>Type and allowed values</th></tr></thead>
<tbody>
<tr><td><code>DEVICE</code></td><td>Any device, used as an marker</td><td><code>state.lastSeen</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.reachable</code></td><td>All devices have this boolean flagg indicating wether the device is currently connected with the gateway device (like the Hub). Accuracy of the information depends on the protocol, e.g. a 433 switch is always reported as reachable because there is no way of telling if the device can hear commands or not. On the other hand if a USB speaker is removed from the Hub, that is detected immediately and reachable flagg is updated near real time.</td><td>boolean<br>read-only</td></tr>
<tr><td><code>ON_OFF</code></td><td>Device can be turned on / off</td><td><code>state.isOn</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>CONTROL_LIGHT</code></td><td>Device can control light (lamp)</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>CONTROL_POWER</code></td><td>Device can control power</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>ACTIVE_POWER</code></td><td>Device can report current power consumption</td><td><code>state.activePower</code></td><td>—</td><td>float Watts<br>read-only</td></tr>
<tr><td><code>ACTIVE_CURRENT</code></td><td>Device can report electric current consumption in amperes</td><td><code>state.activeCurrent</code></td><td>—</td><td>float A<br>read-only</td></tr>
<tr><td><code>MEASURE_POWER</code></td><td>Device can measure power consumption</td><td><code>state.totalPower</code></td><td>—</td><td>float kWh<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.powerToday</code></td><td>—</td><td>float kWh<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.powerYesterday</code></td><td>—</td><td>float kWh<br>read-only</td></tr>
<tr><td><code>PHASE_P</code></td><td>List of electricity power on individual phases</td><td><code>state.phaseP</code></td><td>List index <code>0</code> contains power for phase 1, index <code>1</code> for phase 2, and index <code>2</code> for phase 3.</td><td>List of int Watts<br>read-only</td></tr>
<tr><td><code>PHASE_I</code></td><td>List of electrical current on individual phases</td><td><code>state.phaseI</code></td><td>List index <code>0</code> contains current for phase 1, index <code>1</code> for phase 2, and index <code>2</code> for phase 3.</td><td>List of int Amperes<br>read-only</td></tr>
<tr><td><code>PHASE_U</code></td><td>List of electrical voltage on individual phases</td><td><code>state.phaseU</code></td><td>List index <code>0</code> contains voltage for phase 1, index <code>1</code> for phase 2, and index <code>2</code> for phase 3.</td><td>List of int Volts<br>read-only</td></tr>
<tr><td><code>CONTROL_TEMPERATURE</code></td><td>Device can control temperature</td><td><code>state.targetTemp</code></td><td>—</td><td>float Celsius<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.heatingDemand</code></td><td>How much of the maximum heating power is in use</td><td>float [0.0, 1.0]<br>read-only</td></tr>
<tr><td><code>AIRCON</code></td><td>Device is an aircon</td><td><code>state.mode</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>auto</code></td><td>Automatic mode</td></tr><tr><td><code>dry</code></td><td>Dry mode</td></tr><tr><td><code>cool</code></td><td>Cooling mode</td></tr><tr><td><code>heat</code></td><td>Heating mode</td></tr><tr><td><code>lowheat</code></td><td>Low-heat mode</td></tr></tbody></table><br>writable</td></tr>
<tr><td></td><td></td><td><code>state.fan</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>auto</code></td><td>Automatic fan speed</td></tr><tr><td><code>min</code></td><td>Minimum fan speed</td></tr><tr><td><code>norm</code></td><td>Normal fan speed</td></tr><tr><td><code>max</code></td><td>Maximum fan speed</td></tr></tbody></table><br>writable</td></tr>
<tr><td></td><td></td><td><code>state.hswing</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.lastCommand</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>mode_capabilities</code></td><td>List of supported operating capability profiles for this aircon model. Use these profiles to constrain the values offered for <code>state.mode</code>, <code>state.fan</code>, <code>state.hswing</code>, and <code>state.targetTemp</code> instead of assuming that every aircon supports every option. Most devices return a single profile, but the field is a list. See <code>AirpatrolCapability</code> below.</td><td>List of <code>AirpatrolCapability</code> objects<br>read-only</td></tr>
<tr><td><code>TEMPERATURE</code></td><td>Device can report temperature</td><td><code>state.temperature</code></td><td>—</td><td>float Celsius<br>read-only</td></tr>
<tr><td><code>MOTION</code></td><td>Device detects motion</td><td><code>state.motion</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.lastMotion</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>SELF_MOTION</code></td><td>Device detects own motion</td><td><code>state.selfMotion</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.lastSelfMotion</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.motionLevel</code></td><td>—</td><td>float [0.0, 1.0]<br>read-only</td></tr>
<tr><td><code>PRESSURE</code></td><td>Device can report air pressure</td><td><code>state.pressure</code></td><td>—</td><td>int Pascal<br>read-only</td></tr>
<tr><td><code>HUMIDITY</code></td><td>Device can report air humidity</td><td><code>state.humidity</code></td><td>—</td><td>float Percent<br>read-only</td></tr>
<tr><td><code>MOISTURE</code></td><td>Device can report moisture</td><td><code>state.moisture</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.moistureAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>CONTACT</code></td><td>Device detects contact</td><td><code>state.open</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.lastChange</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>TWILIGHT</code></td><td>Device detects twilight</td><td><code>state.twilight</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.twilightStart</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.twilightStop</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>twilightThreshold</code></td><td>—</td><td>int Lux<br>writable</td></tr>
<tr><td><code>SMOKE</code></td><td>Device detects fire/smoke</td><td><code>state.alert</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.alertStartAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.alertAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>SHUTTER</code></td><td>Device controls curtains</td><td><code>state.motorStatus</code></td><td>Current motor direction.</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>-1</code></td><td>Motor down</td></tr><tr><td><code>0</code></td><td>Motor stopped</td></tr><tr><td><code>1</code></td><td>Motor up</td></tr></tbody></table><br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.calibrating</code></td><td>Indicates that calibration is running.</td><td>boolean<br>read-only</td></tr>
<tr><td><code>DIMMER</code></td><td>Device controls dim level, a bit like remote control but might have no buttons</td><td><code>state.step</code></td><td>—</td><td>float [0.0, 1.0]<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.onoff</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.transitionMsec</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>SIGNAL</code></td><td>Device can give signal. E.g. Bacnet binary value</td><td><code>state.isOn</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>ANALOG_VALUE</code></td><td>Device represents an analog value. E.g. temperature set point as Bacnet analog value</td><td><code>state.value</code></td><td>—</td><td>read-only</td></tr>
<tr><td><code>LIFT</code></td><td>Device can move curtain up/down</td><td><code>state.liftPct</code></td><td>—</td><td>int Percent<br>writable</td></tr>
<tr><td><code>TILT</code></td><td>Device can control the angle of curtain</td><td><code>state.tiltPct</code></td><td>—</td><td>int<br>writable</td></tr>
<tr><td><code>COLOR_RGB</code></td><td>[NOT INUSE?] Device uses RGB for handling color</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>COLOR_HS</code></td><td>Device uses Hue and Saturation for handling color</td><td><code>state.hue</code></td><td>—</td><td>float [0.0, 2*pi]<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.saturation</code></td><td>—</td><td>float [0.0, 1.0]<br>writable</td></tr>
<tr><td><code>COLOR_XY</code></td><td>[NOT INUSE?] Device uses X/Y for handling color</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>COLOR_TEMP</code></td><td>Device can handle color temperature</td><td><code>state.temperature</code></td><td>—</td><td>int [0, 1000000]<br>writable</td></tr>
<tr><td><code>BRIGHTNESS</code></td><td>Device can handle brightness</td><td><code>state.brightness</code></td><td>—</td><td>float [0.0, 1.0]<br>writable</td></tr>
<tr><td><code>TRANSITION</code></td><td>Device can use transition time between state changes</td><td><code>state.transitionMsec</code></td><td>—</td><td>int<br>writable</td></tr>
<tr><td><code>COLOR_LOOP</code></td><td>Device supports color loop</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>VOLUME</code></td><td>Device supports Volume</td><td><code>state.rcData.volume</code></td><td>—</td><td>writable</td></tr>
<tr><td><code>MUTE</code></td><td>Device supports Mute</td><td><code>state.rcData.mute</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>BASS</code></td><td>Device supports Bass</td><td><code>state.rcData.bass</code></td><td>—</td><td>int [0, 10]<br>writable</td></tr>
<tr><td><code>TREBLE</code></td><td>Device supports Treble</td><td><code>state.rcData.treble</code></td><td>—</td><td>int [0, 10]<br>writable</td></tr>
<tr><td><code>LOUDNESS</code></td><td>Device supports Loudness</td><td><code>state.rcData.loudness</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>PLAY_VOLUME</code></td><td>Speaker device play volume</td><td><code>state.playVolume</code></td><td>—</td><td>int [0, 100]<br>writable</td></tr>
<tr><td><code>PLAY_MUTE</code></td><td>Speaker device play mute, <code>true</code> means muted and <code>false</code> means not muted</td><td><code>state.playMute</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>REC_VOLUME</code></td><td>Speaker device record volume (speaker has microphone)</td><td><code>state.recVolume</code></td><td>—</td><td>int [0, 100]<br>writable</td></tr>
<tr><td><code>REC_MUTE</code></td><td>Speaker device record mute, <code>true</code> means muted and <code>false</code> means not muted</td><td><code>state.recMute</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>PLAY_TONE</code></td><td>Write-only tone trigger for speaker devices</td><td><code>state.playTone</code></td><td>Write only, tone is not reported back in state.</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>RING</code></td><td>Play ring tone</td></tr><tr><td><code>RING2</code></td><td>Play alternate ring tone</td></tr><tr><td><code>ALARM</code></td><td>Play alarm tone</td></tr></tbody></table><br>writable</td></tr>
<tr><td><code>PAUSE</code></td><td>Device supports pause</td><td><code>state.isOn</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.avtData.state</code></td><td>—</td><td>—</td></tr>
<tr><td><code>STOP</code></td><td>Device supports stop</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>PLAY</code></td><td>Device supports play</td><td><code>state.avtData.state</code></td><td>—</td><td>—</td></tr>
<tr><td></td><td></td><td><code>state.isOn</code></td><td>—</td><td>—</td></tr>
<tr><td><code>SEEK</code></td><td>Device supports seek</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>NEXT</code></td><td>Device supports next</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>PREVIOUS</code></td><td>Device supports previous</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>ALERT</code></td><td>Device supports indicating some form of alert</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>SIREN</code></td><td>Device can do warning sound</td><td><code>state.sirenOn</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.sirenStartAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.sirenAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.mutedForMs</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>USER_PRESENCE</code></td><td>Device can report user presence</td><td><code>state.user</code></td><td>User id of the last user who logged into this ui and used this hub. Read-only.</td><td>uuid<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.session</code></td><td>True if the ui device is currently connected this Hub otherwise False. Connected means that the Cozify app is running and using this Hub. It takes about 10-20 seconds for the hub to detect that the app was closed. Read-only.</td><td>boolean<br>read-only</td></tr>
<tr><td><code>PUSH_NOTIFICATION</code></td><td>Device can receive push notifications from push notification center in internet</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>REMOTE_CONTROL</code></td><td>Device is a remote control</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>RC_DIMMER</code></td><td>Functional capability allowing the remote control to be selected for a dimmer rule</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>GENERATE_ALERT</code></td><td>Device can generate alert</td><td><code>state.alert</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.alertAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.alertStartAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>CLEAR_ALERT</code></td><td>Device can clear alert e.g. alert is not canceled automatically by the device (or stuck)</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>IDENTIFY</code></td><td>Device can identify itself</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>BATTERY_U</code></td><td>Device can monitor own battery voltage</td><td><code>state.batteryLow</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.batteryV</code></td><td>—</td><td>float Volt<br>read-only</td></tr>
<tr><td><code>BATTERY_C</code></td><td>Device can monitor own battery charging</td><td><code>state.chargingStatus</code></td><td>—</td><td>int<br><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Idle</td></tr><tr><td><code>1</code></td><td>Charging</td></tr><tr><td><code>2</code></td><td>Full</td></tr></tbody></table><br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.chargingStatusAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>LIVE_FEED</code></td><td>Capability to produce live feed</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>RECORD_VIDEO</code></td><td>Capability to record video</td><td><code>medias</code></td><td>—</td><td>List of MediaProfile objects<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.media</code></td><td>—</td><td>string: medias[x].token<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.recording</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.viewerCount</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.connectionState</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>OK</code></td><td>Connection is healthy</td></tr><tr><td><code>DEGRADED</code></td><td>Connection is degraded</td></tr><tr><td><code>LOST</code></td><td>Connection is lost</td></tr></tbody></table><br>read-only</td></tr>
<tr><td><code>RECORD_AUDIO</code></td><td>Capability to record audio</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>LUX</code></td><td>Capability to detect lux</td><td><code>state.lux</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>CO2</code></td><td>Capability to detect CO2 concentration levels</td><td><code>state.co2Ppm</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>CO</code></td><td>Capability to detect carbon monoxide</td><td><code>state.coDetected</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.coAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.coStartAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>UPGRADE</code></td><td>Firmware upgrade capability</td><td><code>state.upgradeStatus</code></td><td>Upgrade availability and progress state. Detailed upgrade metadata can be read through the device-specific upgrade-info response, which includes fields such as <code>fwVersion</code>, <code>swVersion</code>, <code>newFwVersion</code>, <code>newSwVersion</code>, <code>lastUpgrade</code>, <code>autoInstall</code>, <code>pctComplete</code>, and <code>etaMs</code>.</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>OK</code></td><td>No upgrade available</td></tr><tr><td><code>AVAILABLE</code></td><td>New version available</td></tr><tr><td><code>ENQUEUED</code></td><td>Upgrade queued</td></tr><tr><td><code>RUNNING</code></td><td>Upgrade running</td></tr><tr><td><code>BOOTING</code></td><td>Booting after upgrade</td></tr></tbody></table><br>read-only</td></tr>
<tr><td><code>ONVIF_API</code></td><td>ONVIF API capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>FOSCAM_API</code></td><td>Foscam API capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>IAS_ACE</code></td><td>Zigbee IAS ACE capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>IKEA_RC</code></td><td>Ikea device specific capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>IKEA_ONOFF_SWITCH</code></td><td>Ikea device specific capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>HUE_SWITCH</code></td><td>Philips hue switch specific capability</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>BACNET</code></td><td>Remote Bacnet device</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>FLOW_TEMPERATURE</code></td><td>Capability to measure flow temperature degrees Celsius (e.g. water temperature by water meter)</td><td><code>state.flowTemp</code></td><td>—</td><td>float Celsius<br>read-only</td></tr>
<tr><td><code>FLOW</code></td><td>Capability to measure flow l/hour (e.g. water flow by water meter)</td><td><code>state.flow</code></td><td>—</td><td>float<br>read-only</td></tr>
<tr><td><code>FLOW_VOLUME</code></td><td>Capability to measure flow volume l (e.g. total water consumption by water meter)</td><td><code>state.volume</code></td><td>—</td><td>float<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.volumeToday</code></td><td>Calculated daily volume using the Hub time zone.</td><td>float<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.volumeYesterday</code></td><td>Calculated previous-day volume using the Hub time zone.</td><td>float<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.hotWater</code></td><td>Marks the meter as hot-water capable. Set through <code>DeviceMetaCommand</code>.</td><td>boolean<br>writable</td></tr>
<tr><td><code>ZW</code></td><td>ZWave device</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>ZGB</code></td><td>Zigbee Device</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>KNX_AWAY_SWITCH</code></td><td>KNX Home/Away switch, via e.g. mqtt</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>HOME_SWITCH</code></td><td>Home switch mode. Used for remote controls/wallswitches which can be used as Home/Away switch E.g. with Aeotec wallmote quad device setting homeSwitchMode to True will control one scene with first and second buttons and another scene with 3rd and 4th buttons instead of controlling one scene / button Mode is available as boolean value in device attribute homeSwitchMode and can be changed with Device meta command attribute homeSwitchMod</td><td>—</td><td>No dedicated state attribute.</td><td>—</td></tr>
<tr><td><code>VU</code></td><td>Ventilation Unit, e.g. Swegon</td><td><code>state.mode</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Stopped</td></tr><tr><td><code>1</code></td><td>Traveling</td></tr><tr><td><code>2</code></td><td>Away</td></tr><tr><td><code>3</code></td><td>Home</td></tr><tr><td><code>4</code></td><td>Boost</td></tr></tbody></table><br>writable</td></tr>
<tr><td></td><td></td><td><code>state.freshTemperature</code></td><td>—</td><td>float Celsius<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.supplyTemperature</code></td><td>—</td><td>float Celsius<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.extractTemperature</code></td><td>—</td><td>float Celsius<br>read-only</td></tr>
<tr><td><code>VU_FN_FIREPLACE</code></td><td>Ventilation Unit Fireplace function, increases indoor air pressure when there is fire burning in the fireplace</td><td><code>state.fn_fireplace</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>VOC</code></td><td>Capability to detect Volatile Organic Compound</td><td><code>state.vocPpm</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>EXT_THERMOMETER</code></td><td>Capability meaning that the device needs an external temperature meter</td><td><code>thermometer</code></td><td>—</td><td>device id of external thermometer<br>read-only</td></tr>
<tr><td><code>WEARABLE</code></td><td>Capability for indicating that the device is worn, e.g. a watch</td><td><code>state.worn</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.wornAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>PASSIVITY</code></td><td>Capability for detecting passivity of a person, healthcare, e.g. watch</td><td><code>state.passive</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.passiveAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>RSSI</code></td><td>Capability to report RSSI of received message.</td><td><code>state.rssi</code></td><td>—</td><td>int<br>read-only</td></tr>
<tr><td><code>LOW_TEMP</code></td><td>Capability to report low temperature</td><td><code>state.lowTemp</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.lowTempAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>HIGH_TEMP</code></td><td>Capability to report high temperature</td><td>—</td><td>—</td><td>—</td></tr>
<tr><td><code>STARTUP_VALS</code></td><td>Device can restore previous settings after power cycle</td><td><code>startupVals</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td><code>PROXIMITY</code></td><td>Capability to detect the proximity of the device i.e. is it close to hub or not.</td><td><code>state.near</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.nearAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>DETERIORATION</code></td><td>Capability for detecting deterioration in person's condition, healthcare, e.g. watch</td><td><code>state.deterioration</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td><code>DEVICE_HEALTH</code></td><td>Capability for reporting device health. E.g. reset/button stuck</td><td><code>event.alarm (note, not in state!)</code></td><td>—</td><td>DeviceAlarm<br>read-only</td></tr>
<tr><td><code>CONFIGURABLE</code></td><td>This capability means that there is device specific configuration dict available</td><td><code>Get and set via hub command api .../devices/configuration</code></td><td>—</td><td>DeviceConfiguration</td></tr>
<tr><td><code>CONTROL_MODE</code></td><td></td><td><code>state.mode</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Off</td></tr><tr><td><code>1</code></td><td>Auto</td></tr><tr><td><code>2</code></td><td>Heating</td></tr><tr><td><code>3</code></td><td>Cooling</td></tr></tbody></table></td></tr>
<tr><td></td><td></td><td><code>modes</code></td><td>—</td><td>[2,3]</td></tr>
<tr><td><code>CONTROL_PRESET</code></td><td></td><td><code>state.preset</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Comfort</td></tr><tr><td><code>1</code></td><td>Eco</td></tr><tr><td><code>2</code></td><td>Fireplace</td></tr></tbody></table></td></tr>
<tr><td></td><td></td><td><code>presets</code></td><td>—</td><td>[0,1]</td></tr>
<tr><td><code>ROAMING</code></td><td>Capability for indicating that the device is a roaming device which may be automatically paired to multiple hubs.</td><td><code>state.autoPaired</code></td><td>—</td><td>boolean<br>writable</td></tr>
<tr><td><code>RAW_MESSAGE</code></td><td>The original native datagram received from the device</td><td><code>state.datagram</code></td><td>—</td><td>List of integers<br>read-only</td></tr>
<tr><td><code>VOICE_CALL</code></td><td>The device can perform audio calls (cellular, voip etc.)</td><td><code>state.callState</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>1</code></td><td>Idle</td></tr><tr><td><code>2</code></td><td>Calling</td></tr><tr><td><code>3</code></td><td>Ringing</td></tr><tr><td><code>4</code></td><td>In call</td></tr><tr><td><code>5</code></td><td>Hangup</td></tr></tbody></table><br>writable</td></tr>
<tr><td></td><td></td><td><code>state.callAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.callStopAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.callIncoming</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td><code>VOICE_CALL_AUTOANSWER</code></td><td>The device can be configured to automatically answer calls for a period of time. If a list of allowed telephone numbers is given, the call will be automatically answered only if it comes from such a number.</td><td><code>state.callAutoAnswerUntil</code></td><td>—</td><td>timestampms<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.callAutoAnswerFrom</code></td><td>—</td><td>List of telephone numbers as strings<br>writable</td></tr>
<tr><td><code>PHONE_MAKE_CALL</code></td><td>The device can make a phone calls</td><td><code>state.callToNumber</code></td><td>—</td><td>Telephone number as string<br>writable</td></tr>
<tr><td><code>PHONE_RECEIVE_CALL</code></td><td>The device can answer phone calls. When answering the current callState is RINGING and the given callState should be IN_CALL</td><td><code>state.callState</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>1</code></td><td>Idle</td></tr><tr><td><code>2</code></td><td>Calling</td></tr><tr><td><code>3</code></td><td>Ringing</td></tr><tr><td><code>4</code></td><td>In call</td></tr><tr><td><code>5</code></td><td>Hangup</td></tr></tbody></table><br>writable</td></tr>
<tr><td></td><td></td><td><code>state.callFromNumber</code></td><td>—</td><td>Telephone number as string<br>read-only</td></tr>
<tr><td><code>SIM_PIN</code></td><td>The device has PIN/PUK configuration</td><td><code>state.simStatus</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>SIM PIN OK</td></tr><tr><td><code>1</code></td><td>SIM PIN missing</td></tr><tr><td><code>2</code></td><td>SIM missing</td></tr><tr><td><code>3</code></td><td>SIM PIN required</td></tr><tr><td><code>4</code></td><td>SIM PUK required</td></tr><tr><td><code>10</code></td><td>Unknown SIM credentials required</td></tr><tr><td><code>11</code></td><td>SIM modem missing</td></tr></tbody></table><br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.pinAttempts</code></td><td>—</td><td>integer, remaining number of PIN attempts<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.pukAttempts</code></td><td>—</td><td>integer, remaining number of PUK attempts<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.pin</code></td><td>—</td><td>pin code as a string<br>WRITE ONLY</td></tr>
<tr><td></td><td></td><td><code>state.puk</code></td><td>—</td><td>puk code as a string<br>WRITE ONLY</td></tr>
<tr><td><code>STATE_MACHINE</code></td><td></td><td><code>state.currentState</code></td><td>—</td><td>Integer<br><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>2</code></td><td>describing the state of the device.</td></tr></tbody></table></td></tr>
<tr><td></td><td></td><td><code>state.currentStateAt</code></td><td>—</td><td>timestampms</td></tr>
<tr><td><code>G_ALARM</code></td><td>Generic alarm triggered or suppressed</td><td><code>state.gAlarm</code></td><td>—</td><td>boolean<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.gAlarmId</code></td><td>Unique identification that allows to be tracked</td><td>string<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.gAlarmAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.gAlarmMsg</code></td><td>Possible message text attached to triggered alarm</td><td>string<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.gAlarmCode</code></td><td>Device specific code for alarm details</td><td>string<br>read-only</td></tr>
<tr><td><code>SETPOINT</code></td><td>Generic setpoint device</td><td><code>state.setpoint</code></td><td>—</td><td>float Celsius<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.setpointAt</code></td><td>Timestamp for when the setpoint was set</td><td>timestampms<br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.setpointSchedule</code></td><td>Schedule for setpoints to use. List of tuples where first item is starttime for setpoint and second item is the setpoint in effect after the starttime.</td><td>List[Tuple[timestampms, setpoint]]<br>writable</td></tr>
<tr><td><code>SETPOINT_TEMPERATURE</code></td><td>Virtual device containing a temperature setpoint value. This can be used for setting a single generic setpoint which is then used for controlling multiple physical devices by rules.</td><td><code>state.setpointTemp</code></td><td>For example, a room setpoint may drive both floor heating and air conditioning through rules.</td><td>number<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.setpointTempAt</code></td><td>Timestamp for when the setpoint temperature was last updated.</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>VALVE</code></td><td>Percentage value telling how open a valve should be</td><td><code>state.openPct</code></td><td>—</td><td>float percent<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.openPctAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>RW_VALUE</code></td><td>Device represent general read/write value</td><td><code>state.rwValue</code></td><td>—</td><td>number<br>writable</td></tr>
<tr><td></td><td></td><td><code>state.rwValueAt</code></td><td>—</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>FAN_MODE</code></td><td></td><td><code>state.fanMode</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Off</td></tr><tr><td><code>1</code></td><td>Min</td></tr><tr><td><code>2</code></td><td>Low</td></tr><tr><td><code>3</code></td><td>Medium</td></tr><tr><td><code>4</code></td><td>High</td></tr><tr><td><code>5</code></td><td>Max</td></tr><tr><td><code>6</code></td><td>Auto</td></tr></tbody></table><br>writable</td></tr>
<tr><td><code>POWER_SAVE</code></td><td>Device can operate in a device specific powersave mode: NORMAL = 0  # Normal operation mode, power cord connected, PENDING = 1  # Power cord disconnected, pending power save, IN_EFFECT = 2  # Power save in effect, TEMPORARILY_OFF = 3  # Power save is in effect, but we are doing periodic connection</td><td><code>state.powerSave</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>0</code></td><td>Normal</td></tr><tr><td><code>1</code></td><td>Pending</td></tr><tr><td><code>2</code></td><td>In effect</td></tr><tr><td><code>3</code></td><td>Temporarily off</td></tr></tbody></table><br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.powerSaveAt</code></td><td>Timestamp for when the power save changed.</td><td>timestampms<br>read-only</td></tr>
<tr><td><code>G_NOTIFICATION</code></td><td>Device can report device specific notifications. List of strings. String contains timestamp and notification identifier, e.g. [&quot;1723805449000:NOTIF_1_ID&quot;]</td><td><code>state.notifications</code></td><td>—</td><td>List[str]<br>read-only</td></tr>
<tr><td><code>POWER_SOURCE</code></td><td>Device can report which power source it is using. SOURCE_MAINS = Mains power, SOURCE_BATTERY = Battery power</td><td><code>state.powerSource</code></td><td>—</td><td><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody><tr><td><code>1</code></td><td>Mains power</td></tr><tr><td><code>2</code></td><td>Battery power</td></tr></tbody></table><br>read-only</td></tr>
<tr><td></td><td></td><td><code>state.powerSourceAt</code></td><td>Timestamp for when the power source changed.</td><td>timestampms<br>read-only</td></tr>
</tbody></table>

#### Example: building a command from capabilities

Assume a device has these capability values in `status.capabilities.values`:

```json
["DEVICE", "ON_OFF", "CONTROL_LIGHT", "BRIGHTNESS", "COLOR_HS", "TRANSITION"]
```

From that set, a client can conclude:

- the device can be turned on and off with `state.isOn`
- brightness can be set with `state.brightness`
- color must be expressed as `state.hue` and `state.saturation`
- `state.transitionMsec` may be included for gradual changes

A concrete `PUT /devices/command` request body for setting the light to red can therefore look like this:

```json
[
  {
    "type": "CMD_DEVICE",
    "id": "<device-uuid>",
    "state": {
      "type": "STATE_LIGHT",
      "isOn": true,
      "colorMode": "hs",
      "hue": 0.0,
      "saturation": 1.0,
      "brightness": 1.0,
      "transitionMsec": 500
    }
  }
]
```

If the same light had `COLOR_TEMP` but not `COLOR_HS`, the command would use `state.temperature` instead of `state.hue` and `state.saturation`. If a light has both capabilities, the client may offer both color-control models.

### 6.8 Device data

Device data is the broadest object family in the API. The base object is `DeviceStatus`, but the actual returned object type depends on the device type.

Resource layout:

- read object: [Device read object](#device-read-object) returned by `GET /devices`
- write commands: [Device command objects](#device-command-objects) used by `PUT /devices` and `PUT /devices/command`
- configuration objects: [Device configuration objects](#device-configuration-objects) used by `GET /devices/configuration` and `PUT /devices/configuration`
- examples: object and command examples in this section, plus endpoint examples in [5.2 Device endpoints](#52-device-endpoints)

#### Device read object

```python
class DeviceStatus(IdBase):
    type = "DEVICE_STATUS"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.name = None
        self.rwx = 0x1FD
        self.manufacturer = None
        self.model = None
        self.room = None
        self.zones = []
        self.state = None
        self.capabilities = set()
        self.groups = []
        self.timestamp = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for the concrete device-status class. The base class is <code>DEVICE_STATUS</code>, but returned device objects normally use a more specific type such as <code>POWER_SOCKET</code> or <code>MOTION</code>.</td><td>string</td><td><code>POWER_SOCKET</code></td></tr>
<tr><td><code>id</code></td><td>Device identifier.</td><td>UUID string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Device name.</td><td>string</td><td><code>Smart Plug</code></td></tr>
<tr><td><code>rwx</code></td><td>Device-specific visibility, configuration, and command access mask.</td><td>integer</td><td><code>509</code></td></tr>
<tr><td><code>manufacturer</code></td><td>Manufacturer string.</td><td>string</td><td><code>Example Manufacturer</code></td></tr>
<tr><td><code>model</code></td><td>Model string.</td><td>string</td><td><code>Smart Plug</code></td></tr>
<tr><td><code>room</code></td><td>List of room ids. One room is supported in practice.</td><td>array of strings</td><td><code>["&lt;room-uuid&gt;"]</code></td></tr>
<tr><td><code>state</code></td><td>Device-specific state object.</td><td>object</td><td><code>{"type":"STATE_SOCKET",...}</code></td></tr>
<tr><td><code>capabilities</code></td><td>Set of capability identifiers.</td><td>Cozy <code>SET</code> object</td><td><code>{"type":"SET","values":["DEVICE","ON_OFF"]}</code></td></tr>
<tr><td><code>groups</code></td><td>List of group ids. One group is supported in practice.</td><td>array of strings</td><td><code>[]</code></td></tr>
<tr><td><code>absMinHeatLimit</code></td><td>Optional absolute lower bound for writable target temperature on thermostat- and aircon-like devices. Clients should not allow values below this limit when it is present.</td><td>float Celsius or null</td><td><code>10.0</code></td></tr>
<tr><td><code>absMaxHeatLimit</code></td><td>Optional absolute upper bound for writable target temperature on thermostat- and aircon-like devices. Clients should not allow values above this limit when it is present.</td><td>float Celsius or null</td><td><code>30.0</code></td></tr>
<tr><td><code>minHeatLimit</code></td><td>Optional currently active lower bound for writable target temperature on thermostat- and aircon-like devices. This is the effective minimum limit the client should use for the setpoint UI.</td><td>float Celsius or null</td><td><code>16.0</code></td></tr>
<tr><td><code>maxHeatLimit</code></td><td>Optional currently active upper bound for writable target temperature on thermostat- and aircon-like devices. This is the effective maximum limit the client should use for the setpoint UI.</td><td>float Celsius or null</td><td><code>25.0</code></td></tr>
<tr><td><code>timestamp</code></td><td>Polling-helper timestamp used for incremental synchronization. Treat as read-only metadata; do not use it as application data and do not send it in commands.</td><td>timestampms</td><td><code>1710249599000</code></td></tr>
</tbody></table>

`status.type` is not limited to the few device families used in examples. The current codebase contains at least the following concrete `DeviceStatus` subtype values:

```text
HUE_BRIDGE, MOTION, MOISTURE, CO, TWILIGHT, CONTACT, POWER_SOCKET, RELAY, NOSTATE_RELAY,
ALARM_DEVICE, LIGHT, MEDIA_RENDERER, UI_DEVICE, ABSTRACT_REMOTE_CONTROL, REMOTE_CONTROL,
DIMMER, IAS_ACE, DOORBELL, WALLSWITCH, KEYFOB, MULTI_SENSOR, CO2_SENSOR, SMOKE_ALARM,
CAMERA, CELLULAR_MODEM, SHUTTER, THERMOSTAT, SIGNAL, ANALOG_VALUE, RW_VALUE, WATER_METER,
POWER_METER, VENTILATION, HEATING, SPEAKER, FRIDGE, FREEZER, MICROWAVE, DISHWASHER,
WASHING_MACHINE, DRYER, TOWEL_DRYER, AIR_HEAT_PUMP, FLOOR_HEAT, KIUAS, STOVE, OVEN,
APPLIANCE, HOME_AWAY_SWITCH, SIGNAL_REPEATER, SETPOINT_TEMPERATURE, VALVE, EMERGENCY,
WALL_DISPLAY, COZIFY_HUB
```

Not every Hub or installation will use every subtype, but clients should be prepared to receive any of the documented concrete `status.type` values.

The types `CELLULAR_MODEM`, `SPEAKER`, `EMERGENCY`, and `COZIFY_HUB` are `1.14.15+`.

The `rwx` field is a 9-bit access mask for the device. The mask is grouped by caller role, and each role group uses the same three permission bits.

<table>
<thead><tr><th>Role group</th><th>Bit positions</th><th>Bit values</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td>User admin</td><td>8..6</td><td><code>r=4</code>, <code>w=2</code>, <code>x=1</code></td><td>Read, configure, and command rights for callers with User Admin access.</td></tr>
<tr><td>Identified user</td><td>5..3</td><td><code>r=4</code>, <code>w=2</code>, <code>x=1</code></td><td>Read, configure, and command rights for callers with Identified User access.</td></tr>
<tr><td>Guest</td><td>2..0</td><td><code>r=4</code>, <code>w=2</code>, <code>x=1</code></td><td>Read, configure, and command rights for callers with Guest access.</td></tr>
</tbody></table>

Default masks used by the Hub are:

<table>
<thead><tr><th>Mask</th><th>Binary</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0x1FD</code></td><td><code>111 111 101</code></td><td>Default device access. User admin and identified user have full access. Guest can read and command but can not configure.</td></tr>
<tr><td><code>0x16D</code></td><td><code>101 101 101</code></td><td>Default locked access. All roles can read and command, but no one can configure.</td></tr>
</tbody></table>

Practical rules:

- The device owner bypasses the `rwx` mask entirely.
- If a device does not provide `rwx`, the Hub uses the default mask `0x1FD`.
- The Hub checks device access by shifting the requested `r`, `w`, or `x` bit into the correct role group and matching it against `rwx`.
- A device is considered operationally readable only when both user admin and identified user read bits are set.

The public shortcut commands that affect `rwx` are:

<table>
<thead><tr><th>Command type</th><th>Effect on <code>rwx</code></th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>CMD_DEVICE_ACCESS</code></td><td>Sets the exact access mask.</td><td>Direct low-level access-mask command.</td></tr>
<tr><td><code>CMD_DEVICE_VISIBLE</code></td><td>Sets or clears all read bits at once.</td><td><code>visible=true</code> enables read for all documented role groups. <code>visible=false</code> clears read for all of them.</td></tr>
<tr><td><code>CMD_DEVICE_LOCK</code></td><td>Sets or clears all write bits at once.</td><td><code>locked=true</code> removes configuration rights. <code>locked=false</code> restores configuration rights for the documented role groups.</td></tr>
</tbody></table>

If a client tries to make a device unreadable through `CMD_DEVICE_ACCESS`, the Hub may reject that change when the device still belongs to a group or is referenced by a scene or rule.

The `state` field is polymorphic. The base `DeviceState` contains:

```python
class DeviceState(object):
    type = "STATE_DEVICE"

    def __init__(self):
        self.reachable = None
        self.lastSeen = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for the concrete state class. The base class is <code>STATE_DEVICE</code>, but returned state objects usually use a more specific value such as <code>STATE_SOCKET</code>, <code>STATE_LIGHT</code>, or <code>STATE_AIRCON</code>.</td><td>string</td><td><code>STATE_SOCKET</code></td></tr>
<tr><td><code>reachable</code></td><td>Whether the device is currently considered reachable.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>lastSeen</code></td><td>Last communication timestamp.</td><td>timestampms</td><td><code>1710249599000</code></td></tr>
</tbody></table>

Common state families include:

```python
class SwitchableState(DeviceState):
    type = "STATE_SWITCHABLE"

    def __init__(self):
        super().__init__()
        self.isOn = None
```

```python
class SwitchState(SwitchableState):
    type = "STATE_SOCKET"

    def __init__(self):
        super().__init__()
        self.lastChange = None
        self.brightness = None
```

```python
class LightState(SwitchableState):
    type = "STATE_LIGHT"

    def __init__(self):
        super().__init__()
        self.brightness = None
        self.hue = None
        self.saturation = None
        self.temperature = None
        self.colorMode = None
        self.transitionMsec = None
        self.minTemperature = None
        self.maxTemperature = None
```

```python
class MotionSensorState(DeviceState):
    type = "STATE_MOTION"

    def __init__(self):
        super().__init__()
        self.motion = None
        self.lastMotion = None
        self.usePir = None
```

```python
class VentilationState(DeviceState):
    type = "STATE_VENTILATION"

    def __init__(self):
        super().__init__()
        self.mode = None
        self.freshTemperature = None
        self.supplyTemperature = None
        self.extractTemperature = None
        self.humidity = None
        self.co2Ppm = None
        self.vocPpm = None
```

```python
class AirconState(SwitchableState):
    type = "STATE_AIRCON"

    def __init__(self):
        super().__init__()
        self.mode = None
        self.fan = None
        self.targetTemp = None
        self.temperature = None
        self.humidity = None
        self.hswing = None
        self.lastCommand = None
```

```python
class AirpatrolCapability(object):
    type = "AIRPATROL_CAPABILITY"

    def __init__(self):
        self.modes = None
        self.tempMin = None
        self.tempMax = None
        self.fan = None
        self.hswing = None
```

`AirpatrolCapability` is the object used inside the device-level `mode_capabilities` array of AIRCON devices. It describes which operating modes and controls are actually available for the current model or model profile.

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Object type discriminator.</td><td><code>"AIRPATROL_CAPABILITY"</code></td></tr>
<tr><td><code>modes</code></td><td>Supported values for <code>state.mode</code>. Clients should only offer these modes for selection.</td><td>list of strings</td></tr>
<tr><td><code>tempMin</code></td><td>Minimum writable target temperature for this capability profile.</td><td>float Celsius</td></tr>
<tr><td><code>tempMax</code></td><td>Maximum writable target temperature for this capability profile.</td><td>float Celsius</td></tr>
<tr><td><code>fan</code></td><td>Supported values for <code>state.fan</code>. Clients should only offer these fan settings for selection.</td><td>list of strings</td></tr>
<tr><td><code>hswing</code></td><td>If <code>true</code>, the device supports writable horizontal swing control through <code>state.hswing</code>. If <code>false</code>, clients should hide or disable that control.</td><td>boolean</td></tr>
</tbody></table>

Example:

```json
{
  "type": "AIRPATROL_CAPABILITY",
  "modes": ["auto", "heat", "cool", "dry"],
  "tempMin": 16,
  "tempMax": 25,
  "fan": ["min", "norm", "max"],
  "hswing": true
}
```

```python
class SetpointTemperatureState(DeviceState):
    type = "STATE_SETPOINT_TEMPERATURE"

    def __init__(self):
        super().__init__()
        self.setpointTemp = None
        self.setpointTempAt = None
```

<table>
<thead><tr><th>State class</th><th>Serialized <code>state.type</code></th><th>Explanation</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>SwitchableState</code></td><td><code>STATE_SWITCHABLE</code></td><td>Base on/off state with the shared <code>isOn</code> field.</td><td><code>{"type":"STATE_SWITCHABLE","isOn":true}</code></td></tr>
<tr><td><code>SwitchState</code></td><td><code>STATE_SOCKET</code></td><td>On/off socket or relay-like state with <code>isOn</code>, <code>lastChange</code>, and optional <code>brightness</code>.</td><td><code>{"type":"STATE_SOCKET","isOn":true}</code></td></tr>
<tr><td><code>SignalState</code></td><td><code>STATE_SIGNAL</code></td><td>Binary signal state with <code>isOn</code>.</td><td><code>{"type":"STATE_SIGNAL","isOn":false}</code></td></tr>
<tr><td><code>AnalogValueState</code></td><td><code>STATE_ANALOG_VALUE</code></td><td>Generic analog numeric value in <code>value</code>.</td><td><code>{"type":"STATE_ANALOG_VALUE","value":12.5}</code></td></tr>
<tr><td><code>RWValueState</code></td><td><code>STATE_RW_VALUE</code></td><td>Generic read/write numeric value in <code>rwValue</code>.</td><td><code>{"type":"STATE_RW_VALUE","rwValue":42}</code></td></tr>
<tr><td><code>LightState</code></td><td><code>STATE_LIGHT</code></td><td>Light state with <code>isOn</code>, <code>brightness</code>, <code>hue</code>, <code>saturation</code>, <code>temperature</code>, <code>colorMode</code>, <code>transitionMsec</code>, <code>minTemperature</code>, and <code>maxTemperature</code>.</td><td><code>{"type":"STATE_LIGHT","isOn":true,"brightness":0.8}</code></td></tr>
<tr><td><code>MotionSensorState</code></td><td><code>STATE_MOTION</code></td><td>Motion status with <code>motion</code> and <code>lastMotion</code>.</td><td><code>{"type":"STATE_MOTION","motion":false}</code></td></tr>
<tr><td><code>WaterSensorState</code></td><td><code>STATE_MOISTURE</code></td><td>Water-leak state with <code>moisture</code> and <code>moistureAt</code>.</td><td><code>{"type":"STATE_MOISTURE","moisture":false}</code></td></tr>
<tr><td><code>TwilightSensorState</code></td><td><code>STATE_TWILIGHT</code></td><td>Twilight state with <code>twilight</code>, <code>twilightStart</code>, and <code>twilightStop</code>. The configurable <code>twilightThreshold</code> value is not part of the state object; it is a top-level device attribute.</td><td><code>{"type":"STATE_TWILIGHT","twilight":true}</code></td></tr>
<tr><td><code>ContactSwitchState</code></td><td><code>STATE_CONTACT</code></td><td>Contact state with <code>open</code> and <code>lastChange</code>.</td><td><code>{"type":"STATE_CONTACT","open":false}</code></td></tr>
<tr><td><code>RelayState</code></td><td><code>STATE_RELAY</code></td><td>Relay state with <code>isOn</code> and <code>lastChange</code>.</td><td><code>{"type":"STATE_RELAY","isOn":true}</code></td></tr>
<tr><td><code>UiDeviceState</code></td><td><code>STATE_UI_DEVICE</code></td><td>UI-device presence state with <code>user</code> and <code>session</code>.</td><td><code>{"type":"STATE_UI_DEVICE","user":"&lt;user-uuid&gt;","session":true}</code></td></tr>
<tr><td><code>MediaRendererState</code></td><td><code>STATE_MEDIA_RENDERER</code></td><td>Media renderer state with <code>isOn</code>, media metadata in <code>avtData</code>, and rendering-control data in <code>rcData</code>.</td><td><code>{"type":"STATE_MEDIA_RENDERER","isOn":true}</code></td></tr>
<tr><td><code>AbstractRemoteControlState</code></td><td><code>STATE_ABSTRACT_RC</code></td><td>Base remote-control event state with <code>button</code> and <code>modifiers</code>.</td><td><code>{"type":"STATE_ABSTRACT_RC","button":1,"modifiers":["longpress"]}</code></td></tr>
<tr><td><code>RemoteControlState</code></td><td><code>STATE_RC</code></td><td>Remote-control button event state using <code>button</code> and <code>modifiers</code>.</td><td><code>{"type":"STATE_RC","button":2}</code></td></tr>
<tr><td><code>DimmerControlState</code></td><td><code>STATE_DIMMER</code></td><td>Dimmer-control event state with <code>step</code>, <code>onoff</code>, <code>transitionMsec</code>, and optional <code>isOn</code>.</td><td><code>{"type":"STATE_DIMMER","step":0.1}</code></td></tr>
<tr><td><code>IASACEState</code></td><td><code>STATE_IAS_ACE</code></td><td>IAS ACE command state with a single <code>command</code> value such as <code>arm_away</code> or <code>panic</code>.</td><td><code>{"type":"STATE_IAS_ACE","command":"arm_away"}</code></td></tr>
<tr><td><code>DoorbellState</code></td><td><code>STATE_DOORBELL</code></td><td>Doorbell alert state using <code>alert</code>.</td><td><code>{"type":"STATE_DOORBELL","alert":true}</code></td></tr>
<tr><td><code>AlarmState</code></td><td><code>STATE_ALARM</code></td><td>Binary alarm-output state based on <code>isOn</code>.</td><td><code>{"type":"STATE_ALARM","isOn":true}</code></td></tr>
<tr><td><code>WallswitchState</code></td><td><code>STATE_WALLSWITCH</code></td><td>Wall-switch button event state using <code>button</code> and <code>modifiers</code>.</td><td><code>{"type":"STATE_WALLSWITCH","button":1}</code></td></tr>
<tr><td><code>KeyfobState</code></td><td><code>STATE_KEYFOB</code></td><td>Keyfob button event state using <code>button</code> and <code>modifiers</code>.</td><td><code>{"type":"STATE_KEYFOB","button":3}</code></td></tr>
<tr><td><code>MultiSensorState</code></td><td><code>STATE_MULTI_SENSOR</code></td><td>Multi-sensor state with <code>temperature</code> and <code>humidity</code>.</td><td><code>{"type":"STATE_MULTI_SENSOR","temperature":21.5,"humidity":41.0}</code></td></tr>
<tr><td><code>CO2SensorState</code></td><td><code>STATE_CO2_SENSOR</code></td><td>Carbon-dioxide state with <code>co2Ppm</code>.</td><td><code>{"type":"STATE_CO2_SENSOR","co2Ppm":650}</code></td></tr>
<tr><td><code>COSensorState</code></td><td><code>STATE_CO_SENSOR</code></td><td>Carbon-monoxide state with <code>coDetected</code>, <code>coAt</code>, and <code>coStartAt</code>.</td><td><code>{"type":"STATE_CO_SENSOR","coDetected":false}</code></td></tr>
<tr><td><code>SmokeAlarmState</code></td><td><code>STATE_SMOKE_ALARM</code></td><td>Smoke/fire alarm state with <code>alert</code>, <code>alertAt</code>, <code>alertStartAt</code>, <code>sirenOn</code>, <code>sirenAt</code>, <code>sirenStartAt</code>, and <code>mutedForMs</code>.</td><td><code>{"type":"STATE_SMOKE_ALARM","alert":false,"sirenOn":false}</code></td></tr>
<tr><td><code>CameraState</code></td><td><code>STATE_CAMERA</code></td><td>Camera state with <code>media</code>, <code>recording</code>, <code>viewerCount</code>, and <code>connectionState</code>.</td><td><code>{"type":"STATE_CAMERA","recording":true}</code></td></tr>
<tr><td><code>CellularModemState</code></td><td><code>STATE_CELLULAR_MODEM</code></td><td>Cellular-modem state with SIM status, call state, call metadata, auto-answer settings, and RSSI. <strong>1.14.15+ only.</strong></td><td><code>{"type":"STATE_CELLULAR_MODEM","simStatus":0,"callState":1}</code></td></tr>
<tr><td><code>AirconState</code></td><td><code>STATE_AIRCON</code></td><td>Air-conditioner state with mode, fan, temperature, humidity, and swing fields.</td><td><code>{"type":"STATE_AIRCON","mode":"cool","fan":"auto"}</code></td></tr>
<tr><td><code>ShutterState</code></td><td><code>STATE_SHUTTER</code></td><td>Shutter state with <code>liftPct</code>, <code>tiltPct</code>, <code>motorStatus</code>, <code>calibrating</code>, and optional <code>isOn</code> for open/close semantics.</td><td><code>{"type":"STATE_SHUTTER","liftPct":50,"motorStatus":0,"calibrating":false}</code></td></tr>
<tr><td><code>ThermostatState</code></td><td><code>STATE_THERMOSTAT</code></td><td>Thermostat state with measured <code>temperature</code>, desired <code>targetTemp</code>, and <code>heatingDemand</code>.</td><td><code>{"type":"STATE_THERMOSTAT","temperature":21.0,"targetTemp":22.0}</code></td></tr>
<tr><td><code>WaterMeterState</code></td><td><code>STATE_WATER_METER</code></td><td>Water-meter state with <code>temperature</code>, <code>flowTemp</code>, <code>flow</code>, <code>volume</code>, <code>volumeToday</code>, <code>volumeYesterday</code>, and <code>hotWater</code>.</td><td><code>{"type":"STATE_WATER_METER","volume":1234,"volumeToday":45,"hotWater":true}</code></td></tr>
<tr><td><code>PowerMeterState</code></td><td><code>STATE_POWER_METER</code></td><td>Power-meter state with <code>totalPower</code>, <code>activePower</code>, <code>powerToday</code>, and <code>powerYesterday</code>.</td><td><code>{"type":"STATE_POWER_METER","activePower":120.5}</code></td></tr>
<tr><td><code>CentralHeatingState</code></td><td><code>STATE_HEATING</code></td><td>Central-heating state with integer <code>mode</code> and <code>preset</code>.</td><td><code>{"type":"STATE_HEATING","mode":1,"preset":2}</code></td></tr>
<tr><td><code>SpeakerState</code></td><td><code>STATE_SPEAKER</code></td><td>Speaker state with playback and recording controls: <code>playVolume</code>, <code>playMute</code>, <code>recVolume</code>, <code>recMute</code>, and <code>playTone</code>. <strong>1.14.15+ only.</strong></td><td><code>{"type":"STATE_SPEAKER","playVolume":40,"playMute":false}</code></td></tr>
<tr><td><code>VentilationState</code></td><td><code>STATE_VENTILATION</code></td><td>Ventilation-unit state with mode and measured temperatures.</td><td><code>{"type":"STATE_VENTILATION","mode":3}</code></td></tr>
<tr><td><code>HubDeviceState</code></td><td><code>STATE_HUB</code></td><td>Hub-device marker state with no shared dedicated fields beyond the base <code>DeviceState</code> fields. <strong>1.14.15+ only.</strong></td><td><code>{"type":"STATE_HUB","reachable":true}</code></td></tr>
<tr><td><code>ValveState</code></td><td><code>STATE_VALVE</code></td><td>Valve-opening state with <code>openPct</code>.</td><td><code>{"type":"STATE_VALVE","openPct":0.5}</code></td></tr>
<tr><td><code>EmergencyState</code></td><td><code>STATE_EMERGENCY</code></td><td>Emergency-alert state with <code>alert</code>, <code>alertAt</code>, and <code>alertStartAt</code>. <strong>1.14.15+ only.</strong></td><td><code>{"type":"STATE_EMERGENCY","alert":true}</code></td></tr>
<tr><td><code>SetpointTemperatureState</code></td><td><code>STATE_SETPOINT_TEMPERATURE</code></td><td>Temperature setpoint state with setpoint value and timestamp.</td><td><code>{"type":"STATE_SETPOINT_TEMPERATURE","setpointTemp":21.0}</code></td></tr>
<tr><td><code>ShellyState</code></td><td><code>STATE_SHELLY</code></td><td>Shelly-specific marker state with no shared dedicated fields in the common data model.</td><td><code>{"type":"STATE_SHELLY","reachable":true}</code></td></tr>
</tbody></table>

Device capabilities are documented in section 6.7.

The practical interpretation is:

- `status.type` tells what device object family the client received
- `status.state.type` tells what state object family the device uses
- `capabilities` tells what the client can present or command

#### Camera read objects and stream-profile data

Camera devices use concrete `status.type` value `CAMERA`.

Scope note:

- all camera devices use the common camera stream model: `cameraId`, `medias`, `state.media`, `state.recording`, `state.viewerCount`, and `state.connectionState`
- Foscam-oriented setup fields are `ssid`, `wifiConnected`, and `credentialStatus`
- the richer onboarding and Wi-Fi-management flows documented later are also Foscam-specific

`CameraStatus` adds these fields on top of the generic device object:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>cameraId</code></td><td>Camera identifier used by the video service URLs. This is not always the same as the Hub device <code>id</code>. Applies to all cameras.</td><td>UUID string or null</td><td><code>2cb497c6-1616-4cea-93dd-d9b9f558f110</code></td></tr>
<tr><td><code>medias</code></td><td>Available stream profiles reported by the camera. The selected profile is referenced through <code>state.media</code>. Applies to all cameras that expose selectable stream profiles.</td><td>array of <code>MediaProfile</code></td><td><code>[{"type":"MEDIA_PROFILE","token":"0",...}]</code></td></tr>
<tr><td><code>ssid</code></td><td>SSID of the Wi-Fi network currently configured on the camera, when known. Foscam-oriented field used by the Foscam Wi-Fi setup flow.</td><td>string or null</td><td><code>Office WiFi</code></td></tr>
<tr><td><code>wifiConnected</code></td><td>Whether the camera is currently using Wi-Fi instead of a wired connection. Foscam-oriented field used by the Foscam Wi-Fi setup flow.</td><td>boolean or null</td><td><code>true</code></td></tr>
<tr><td><code>credentialStatus</code></td><td>Status of the credentials the Hub is currently using for the Foscam camera. This field is Foscam-specific, not a generic camera attribute.</td><td>string enum or null</td><td><code>OK</code></td></tr>
</tbody></table>

For Foscam cameras, `credentialStatus` uses these values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>OK</code></td><td>The stored credentials are valid and have the required rights for normal camera operations.</td></tr>
<tr><td><code>INSUFFICIENT</code></td><td>The stored credentials log in successfully, but do not have enough privileges for all management operations.</td></tr>
<tr><td><code>DEFAULT</code></td><td>The camera is still using default credentials.</td></tr>
<tr><td><code>ERROR</code></td><td>The stored credentials are missing or invalid.</td></tr>
</tbody></table>

Stream profiles are represented by `MediaProfile` objects:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>MEDIA_PROFILE</code></td></tr>
<tr><td><code>name</code></td><td>Human-readable label for the stream profile.</td><td>string</td><td><code>HD (15 fps @ 1024k)</code></td></tr>
<tr><td><code>token</code></td><td>Unique profile token used in <code>state.media</code> and live-stream selection logic.</td><td>string</td><td><code>sub_1</code></td></tr>
<tr><td><code>width</code></td><td>Video width in pixels.</td><td>integer or null</td><td><code>1280</code></td></tr>
<tr><td><code>height</code></td><td>Video height in pixels.</td><td>integer or null</td><td><code>720</code></td></tr>
<tr><td><code>bitrate</code></td><td>Configured bitrate as reported by the camera.</td><td>string or null</td><td><code>1024</code></td></tr>
<tr><td><code>framerate</code></td><td>Configured frame rate as reported by the camera.</td><td>string or null</td><td><code>15</code></td></tr>
<tr><td><code>videoParams</code></td><td>Resolved SDP-style video encoding parameters for the profile.</td><td><code>SDP_PARAMS</code> or null</td><td><pre><code class="language-json">{
  "type": "SDP_PARAMS",
  "payload": "96",
  "encoding": "H264",
  "format": "90000"
}</code></pre></td></tr>
<tr><td><code>audioParams</code></td><td>Resolved SDP-style audio encoding parameters for the profile when audio is present.</td><td><code>SDP_PARAMS</code> or null</td><td><pre><code class="language-json">{
  "type": "SDP_PARAMS",
  "payload": "0",
  "encoding": "PCMU",
  "format": "8000"
}</code></pre></td></tr>
</tbody></table>

`videoParams` and `audioParams` use `SDPMediaParameters` objects:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>SDP_PARAMS</code></td></tr>
<tr><td><code>payload</code></td><td>Payload-type identifier used in SDP / RTP signaling.</td><td>string or null</td><td><code>96</code></td></tr>
<tr><td><code>encoding</code></td><td>Codec name.</td><td>string or null</td><td><code>H264</code></td></tr>
<tr><td><code>format</code></td><td>Format-specific parameter string, often clock rate or codec format.</td><td>string or null</td><td><code>90000</code></td></tr>
</tbody></table>

For camera stream settings, the read/write/refresh model is:

- all cameras: read the available profiles from `status.medias`
- all cameras: read the currently selected profile token from `status.state.media`
- all cameras: change the selected profile with `CMD_DEVICE` carrying `state.type = "STATE_CAMERA"` and a new `state.media`
- all cameras: refresh the camera-reported profile list and related metadata with `REFRESH_DEVICE`
- Foscam cameras additionally refresh Wi-Fi and credential details such as `ssid`, `wifiConnected`, and `credentialStatus`

Example Foscam camera object:

```json
{
  "type": "CAMERA",
  "id": "<camera-device-uuid>",
  "name": "Front door camera",
  "cameraId": "2cb497c6-1616-4cea-93dd-d9b9f558f110",
  "ssid": "Office WiFi",
  "wifiConnected": true,
  "credentialStatus": "OK",
  "medias": [
    {
      "type": "MEDIA_PROFILE",
      "name": "HD (15 fps @ 1024k)",
      "token": "0",
      "width": 1280,
      "height": 720,
      "bitrate": "1024",
      "framerate": "15",
      "videoParams": {
        "type": "SDP_PARAMS",
        "payload": "96",
        "encoding": "H264",
        "format": "90000"
      },
      "audioParams": null
    }
  ],
  "state": {
    "type": "STATE_CAMERA",
    "reachable": true,
    "media": "0",
    "recording": false,
    "viewerCount": 0,
    "connectionState": "OK"
  }
}
```

#### Device command objects

The main write-side device command objects are:

```python
class DeviceMetaCommand(UiCmd):
    type = "CMD_DEVICE_META"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.name = None
        self.room = None
        self.minHeatLimit = None
        self.maxHeatLimit = None


class DeviceCommand(UiCmd):
    type = "CMD_DEVICE"

    def __init__(self, device_id=None, state=None):
        super().__init__(device_id)
        self.state = state


class DeviceOnCommand(UiCmd):
    type = "CMD_DEVICE_ON"


class DeviceOffCommand(UiCmd):
    type = "CMD_DEVICE_OFF"


class DeviceAccessCommand(UiCmd):
    type = "CMD_DEVICE_ACCESS"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.rwx = None


class DeviceVisibilityCommand(UiCmd):
    type = "CMD_DEVICE_VISIBLE"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.visible = None


class DeviceLockedCommand(UiCmd):
    type = "CMD_DEVICE_LOCK"

    def __init__(self, device_id=None):
        super().__init__(device_id)
        self.locked = None


class RefreshDevice(UiCmd):
    type = "REFRESH_DEVICE"
```

`DeviceMetaCommand` is used by `PUT /devices`:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_DEVICE_META</code></td></tr>
<tr><td><code>id</code></td><td>Target device id.</td><td>UUID string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>New device name.</td><td>string or null</td><td><code>Living room lamp</code></td></tr>
<tr><td><code>room</code></td><td>Replacement room-id list. One room is supported in practice.</td><td>array of strings or null</td><td><code>["&lt;room-uuid&gt;"]</code></td></tr>
<tr><td><code>minHeatLimit</code></td><td>Optional replacement lower setpoint limit for devices that expose configurable temperature limits. When present, the value must stay within the device's absolute limits.</td><td>float Celsius or null</td><td><code>16.0</code></td></tr>
<tr><td><code>maxHeatLimit</code></td><td>Optional replacement upper setpoint limit for devices that expose configurable temperature limits. When present, the value must stay within the device's absolute limits.</td><td>float Celsius or null</td><td><code>25.0</code></td></tr>
</tbody></table>

`DeviceCommand` is the generic partial-state command used by `PUT /devices/command` and also by `PUT /groups/command` when targeting a group:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_DEVICE</code></td></tr>
<tr><td><code>id</code></td><td>Target device id, or group id when used through <code>/groups/command</code>.</td><td>UUID string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>state</code></td><td>Partial target state. Any state attribute omitted from the JSON, or explicitly set to <code>null</code>, is ignored by the Hub instead of being overwritten.</td><td>device-specific state object</td><td><code>{"type":"STATE_LIGHT","isOn":true,"brightness":0.8}</code></td></tr>
</tbody></table>

Example partial device command:

```json
{
  "type": "CMD_DEVICE",
  "id": "<device-uuid>",
  "state": {
    "type": "STATE_LIGHT",
    "isOn": true,
    "brightness": 0.8
  }
}
```

The simple on/off command types are:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload shape</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CMD_DEVICE_ON</code></td><td>Turns the target device on.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_DEVICE_ON",
  "id": "&lt;device-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>CMD_DEVICE_OFF</code></td><td>Turns the target device off.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_DEVICE_OFF",
  "id": "&lt;device-uuid&gt;"
}</code></pre></td></tr>
</tbody></table>

The access-related device commands are:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload fields</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CMD_DEVICE_ACCESS</code></td><td>Sets the exact <code>rwx</code> mask.</td><td><code>id</code>, <code>rwx</code></td><td><pre><code class="language-json">{
  "type": "CMD_DEVICE_ACCESS",
  "id": "&lt;device-uuid&gt;",
  "rwx": 365
}</code></pre></td></tr>
<tr><td><code>CMD_DEVICE_VISIBLE</code></td><td>Convenience command for toggling device visibility.</td><td><code>id</code>, <code>visible</code></td><td><pre><code class="language-json">{
  "type": "CMD_DEVICE_VISIBLE",
  "id": "&lt;device-uuid&gt;",
  "visible": false
}</code></pre></td></tr>
<tr><td><code>CMD_DEVICE_LOCK</code></td><td>Convenience command for toggling configuration lock state.</td><td><code>id</code>, <code>locked</code></td><td><pre><code class="language-json">{
  "type": "CMD_DEVICE_LOCK",
  "id": "&lt;device-uuid&gt;",
  "locked": true
}</code></pre></td></tr>
</tbody></table>

The refresh command is:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload shape</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>REFRESH_DEVICE</code></td><td>Requests the target device to refresh its state from the underlying hardware or service.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "REFRESH_DEVICE",
  "id": "&lt;device-uuid&gt;"
}</code></pre></td></tr>
</tbody></table>

For all cameras, `REFRESH_DEVICE` is the explicit refresh path for camera-reported metadata such as:

- currently available stream profiles in `medias`
- the currently selected stream token in `state.media`
- credential status and other camera status details

For Foscam cameras, the same refresh path also updates Foscam-specific network-management data such as:

- Wi-Fi connection state and SSID

`REFRESH_DEVICE` is dispatched asynchronously. Clients should observe the updated values through later poll responses instead of expecting an immediate response payload.

The ignore-management device commands are:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload fields</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CMD_IGNORE_DEVICE</code></td><td>Removes the target device from the Hub and marks it ignored so it is not automatically paired again.</td><td><code>id</code></td><td><pre><code class="language-json">{
  "type": "CMD_IGNORE_DEVICE",
  "id": "&lt;device-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>CMD_RESET_IGNORED</code></td><td>Clears the Hub's ignored-device registry so ignored devices can be discovered and paired again.</td><td><code>type</code> only</td><td><pre><code class="language-json">{
  "type": "CMD_RESET_IGNORED"
}</code></pre></td></tr>
</tbody></table>

These two commands are exceptions to the normal asynchronous device-command behavior:

- they are handled synchronously
- they require `USER_ADMIN_ACCESS`
- `CMD_IGNORE_DEVICE` targets one device id
- `CMD_RESET_IGNORED` is Hub-wide and does not take a device id

#### Device configuration objects

Devices with capability `CONFIGURABLE` expose their editable settings through `Configuration`, `ConfigTemplate`, and
`ConfigItem`.

```python
class ConfigItem(object):
    type = "CFG_ITEM"

    def __init__(self, field_type=None):
        super().__init__()
        self.name = None
        self.fieldType = field_type
        self.description = None
        self.help = None
        self.minValue = None
        self.maxValue = None
        self.defaultValue = None
        self.options = None
        self.readOnly = None
```

```python
class ConfigTemplate(object):
    type = "CFG_TEMPLATE"

    def __init__(self):
        super().__init__()
        self.parameters = []
```

```python
class Configuration(UiCmd):
    type = "CONFIGURATION"

    def __init__(self, the_id=None):
        super().__init__(the_id)
        self.template = None
        self.parameters = {}
```

`ConfigItem` describes one editable configuration parameter:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>CFG_ITEM</code></td></tr>
<tr><td><code>name</code></td><td>Parameter key used in <code>Configuration.parameters</code>.</td><td>string</td><td><code>brightness_point_type</code></td></tr>
<tr><td><code>fieldType</code></td><td>Defines what kind of value the parameter stores and how the UI should render it.</td><td>string</td><td><code>OPTION</code></td></tr>
<tr><td><code>description</code></td><td>Short human-readable label for the field.</td><td>string</td><td><code>Brightness point object type</code></td></tr>
<tr><td><code>help</code></td><td>Longer explanatory text for the field.</td><td>string or null</td><td><code>Select which BACnet object type is used for the brightness point.</code></td></tr>
<tr><td><code>minValue</code></td><td>Lower bound for the accepted value when the parameter type supports ranges.</td><td>number or null</td><td><code>1</code></td></tr>
<tr><td><code>maxValue</code></td><td>Upper bound for the accepted value when the parameter type supports ranges.</td><td>number or null</td><td><code>4194303</code></td></tr>
<tr><td><code>defaultValue</code></td><td>Default value to use when the configuration does not currently contain a value for this parameter.</td><td>any or null</td><td><code>analogOutput</code></td></tr>
<tr><td><code>options</code></td><td>Mapping from visible option label to stored value. Used only for <code>OPTION</code> fields.</td><td>object or null</td><td><code>{"Analog Output":"analogOutput"}</code></td></tr>
<tr><td><code>readOnly</code></td><td>If <code>true</code>, the parameter can be shown but not edited.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

Supported `fieldType` values and UI rendering guidance:

<table>
<thead><tr><th>Field type</th><th>Stored value</th><th>Rendering guidance</th><th>Example</th></tr></thead>
<tbody>
<tr><td><code>BOOL</code></td><td>boolean</td><td>Render a checkbox, switch, or similar on/off control.</td><td><code>user_group_cmds = true</code></td></tr>
<tr><td><code>NUMBER</code></td><td>number</td><td>Render a numeric input. Apply <code>minValue</code> and <code>maxValue</code> when present.</td><td><code>brightness_point_id = 7</code></td></tr>
<tr><td><code>TEXT</code></td><td>string</td><td>Render a free-text input.</td><td><code>status_tpc = "home/lights/1/state"</code></td></tr>
<tr><td><code>OPTION</code></td><td>selected option value</td><td>Render a dropdown, radio group, or segmented selector using <code>options</code> keys as labels and the mapped values as stored values.</td><td><code>brightness_point_type = "analogOutput"</code></td></tr>
<tr><td><code>DICTIONARY</code></td><td>object</td><td>Render a structured key-value editor or a validated JSON/object editor.</td><td><code>{"1":1.0,"2":1.0,"3":1.0}</code></td></tr>
<tr><td><code>DEVICE</code></td><td>device id string or <code>null</code></td><td>Render a device picker and store the selected device id.</td><td><code>thermometer = "&lt;device-uuid&gt;"</code></td></tr>
</tbody></table>

`ConfigTemplate` contains the parameter descriptors used to build the editing UI:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>CFG_TEMPLATE</code></td></tr>
<tr><td><code>parameters</code></td><td>Ordered list of configuration parameter descriptors. The UI should render the parameters in this order.</td><td>array of <code>ConfigItem</code></td><td><code>[{"type":"CFG_ITEM","name":"brightness_point_type"}, {"type":"CFG_ITEM","name":"brightness_point_id"}]</code></td></tr>
</tbody></table>

`Configuration` contains the current editable state for one device:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>CONFIGURATION</code></td></tr>
<tr><td><code>id</code></td><td>Target device id.</td><td>UUID string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>template</code></td><td>Optional configuration template. Present when configuration is read with <code>templates=true</code>.</td><td><code>ConfigTemplate</code> or null</td><td><code>{"type":"CFG_TEMPLATE","parameters":[...]}</code></td></tr>
<tr><td><code>parameters</code></td><td>Current configuration values keyed by parameter name.</td><td>object</td><td><code>{"brightness_point_type":"analogOutput","brightness_point_id":7}</code></td></tr>
</tbody></table>

How to render an editable device configuration view:

1. Read the configuration with `GET /devices/configuration?deviceId=<device-uuid>&templates=true`.
2. Iterate `configuration.template.parameters` in order.
3. For each parameter, use `description` as the visible field label and `help` as secondary explanation.
4. Determine the current field value from `configuration.parameters[parameter.name]`. If no current value is present, initialize the editor from `defaultValue`.
5. Choose the editor by `fieldType`.
6. Apply `minValue`, `maxValue`, and `options` constraints when they are present.
7. If `readOnly=true`, show the value but disable editing.
8. Save the edited values with `PUT /devices/configuration` using the same device id.

When saving, the simplest client behavior is to send the full edited `parameters` mapping. The backend also tolerates
missing keys by ignoring them, so clients may omit untouched parameters if needed.

Example configuration object:

```json
{
  "type": "CONFIGURATION",
  "id": "<device-uuid>",
  "template": {
    "type": "CFG_TEMPLATE",
    "parameters": [
      {
        "type": "CFG_ITEM",
        "name": "brightness_point_type",
        "fieldType": "OPTION",
        "description": "Brightness point object type",
        "help": "Select which BACnet object type is used for the brightness point.",
        "defaultValue": "analogOutput",
        "options": {
          "Analog Value": "analogValue",
          "Analog Input": "analogInput",
          "Analog Output": "analogOutput"
        },
        "readOnly": false
      },
      {
        "type": "CFG_ITEM",
        "name": "brightness_point_id",
        "fieldType": "NUMBER",
        "description": "Brightness point object id",
        "help": "Object instance number for the BACnet brightness point.",
        "readOnly": false
      }
    ]
  },
  "parameters": {
    "brightness_point_type": "analogOutput",
    "brightness_point_id": 7
  }
}
```

Example:

```json
{
  "type": "POWER_SOCKET",
  "id": "<device-uuid>",
  "name": "Smart Plug",
  "rwx": 509,
  "manufacturer": "Example Manufacturer",
  "model": "Smart Plug",
  "room": ["<room-uuid>"],
  "groups": [],
  "capabilities": {
    "type": "SET",
    "values": ["DEVICE", "ON_OFF", "CONTROL_POWER"]
  },
  "state": {
    "type": "STATE_SOCKET",
    "reachable": true,
    "lastSeen": 1710249599000,
    "isOn": true,
    "lastChange": 1710249595000,
    "brightness": null
  },
  "timestamp": 1710249599000
}
```

### 6.9 Group data

The shared group object is `Group`.

Resource layout:

- read object: [Group read object](#group-read-object) returned by `GET /groups`
- write commands: [Group command objects](#group-command-objects) used by `PUT /groups` and `PUT /groups/command`
- examples: object examples in this section and endpoint examples in [5.3 Group endpoints](#53-group-endpoints)

#### Group read object

```python
class Group(IdBase):
    type = "GROUP"

    def __init__(self, group_id=None):
        super().__init__(group_id)
        self.name = None
        self.state = None
        self.members = []
        self.room = []
        self.zones = []
        self.minCapabilities = set()
        self.maxCapabilities = set()
        self.group_address = None
        self.timestamp = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>GROUP</code></td></tr>
<tr><td><code>id</code></td><td>Group identifier.</td><td>UUID string</td><td><code>&lt;group-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Group name.</td><td>string</td><td><code>Living room lights</code></td></tr>
<tr><td><code>state</code></td><td>Representative group state.</td><td>object</td><td><code>{"type":"STATE_LIGHT","isOn":true}</code></td></tr>
<tr><td><code>members</code></td><td>List of device ids in the group.</td><td>array of strings</td><td><code>["&lt;device-uuid&gt;","&lt;device-uuid-2&gt;"]</code></td></tr>
<tr><td><code>room</code></td><td>List of room ids. One room is supported.</td><td>array of strings</td><td><code>["&lt;room-uuid&gt;"]</code></td></tr>
<tr><td><code>minCapabilities</code></td><td>Intersection of member-device capabilities.</td><td>Cozy <code>SET</code> object</td><td><code>{"type":"SET","values":["DEVICE","ON_OFF"]}</code></td></tr>
<tr><td><code>maxCapabilities</code></td><td>Union of member-device capabilities.</td><td>Cozy <code>SET</code> object</td><td><code>{"type":"SET","values":["DEVICE","ON_OFF","BRIGHTNESS"]}</code></td></tr>
<tr><td><code>group_address</code></td><td>Protocol-specific group address, for example for Zigbee.</td><td>integer or null</td><td><code>4711</code></td></tr>
<tr><td><code>timestamp</code></td><td>Polling-helper timestamp used for incremental synchronization. Treat as read-only metadata; do not use it as application data and do not send it in commands.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
</tbody></table>

`state` is not an aggregate with full conflict resolution. It is described in code as the state of one member device, typically the first reachable one.

At the UI level, the intended design is that a group replaces its member devices in the main device presentation. Individual devices that belong to a group are normally shown only in the group's detail view.

When groups are created or updated through `GroupCommand`, missing or `null` fields are not changed.

#### Group command objects

The main write-side group command objects are:

```python
class GroupCommand(UiCmd):
    type = "CMD_GROUP"

    def __init__(self, group_id=None):
        super().__init__(group_id)
        self.name = None
        self.members = None
        self.room = None


class GroupOnCommand(UiCmd):
    type = "CMD_GROUP_ON"


class GroupOffCommand(UiCmd):
    type = "CMD_GROUP_OFF"
```

`GroupCommand` is used by `PUT /groups`:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_GROUP</code></td></tr>
<tr><td><code>id</code></td><td>Target group id. Use <code>null</code> to create a new group.</td><td>UUID string or null</td><td><code>null</code></td></tr>
<tr><td><code>name</code></td><td>New group name.</td><td>string or null</td><td><code>Living room lights</code></td></tr>
<tr><td><code>members</code></td><td>Replacement member-device list.</td><td>array of strings or null</td><td><code>["&lt;device-uuid&gt;","&lt;device-uuid-2&gt;"]</code></td></tr>
<tr><td><code>room</code></td><td>Replacement room-id list. One room is supported in practice.</td><td>array of strings or null</td><td><code>["&lt;room-uuid&gt;"]</code></td></tr>
</tbody></table>

The simple group command types are:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload shape</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CMD_GROUP_ON</code></td><td>Turns the target group on.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_GROUP_ON",
  "id": "&lt;group-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>CMD_GROUP_OFF</code></td><td>Turns the target group off.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_GROUP_OFF",
  "id": "&lt;group-uuid&gt;"
}</code></pre></td></tr>
</tbody></table>

`PUT /groups/command` may also use the `CMD_DEVICE` payload shape documented in [Device command objects](#device-command-objects). In that case, the target `id` is the group id and the partial `state` is applied through the group.

Example:

```json
{
  "type": "GROUP",
  "id": "<group-uuid>",
  "name": "Living room lights",
  "state": {
    "type": "STATE_LIGHT",
    "isOn": true,
    "brightness": 0.8
  },
  "members": ["<device-uuid>", "<device-uuid-2>"],
  "room": ["<room-uuid>"],
  "minCapabilities": {
    "type": "SET",
    "values": ["DEVICE", "ON_OFF", "CONTROL_LIGHT"]
  },
  "maxCapabilities": {
    "type": "SET",
    "values": ["DEVICE", "ON_OFF", "CONTROL_LIGHT", "BRIGHTNESS"]
  },
  "group_address": 4711,
  "timestamp": 1710249600000
}
```

### 6.10 Rule data

The rule-related object family is split into configuration, activation logic, runtime data, and optional proposals:

- `RuleTemplate`
- `RuleConfig`
- `RuleOnConfig`
- `RuleData`
- `RuleProposal`

Template text fields such as `name`, `description`, `summary`, field `description`, and field `help` are already localized by the Hub according to the request `Accept-Language` header. The values returned to the client are ordinary strings in that language.

Resource layout:

- configuration objects: [Rule configuration objects](#rule-configuration-objects) used by `GET /rules/templates`, `PUT /rules`, and `PUT /rules/onConfiguration`
- read objects: [Rule read objects](#rule-read-objects) returned by `GET /rules` and `POST /rules/proposals`
- write commands: [Rule command objects](#rule-command-objects) used by `PUT /rules/command`
- typed helper values: [Typed rule value objects](#typed-rule-value-objects)
- examples: object examples in this section and endpoint examples in [5.4 Rule endpoints](#54-rule-endpoints)

#### Rule configuration objects

```python
class RuleTemplate(object):
    type = "TEMPLATE"

    def __init__(self):
        self.category = None
        self.description = None
        self.summary = None
        self.configType = None
        self.name = None
        self.priority = None
        self.icon = None
        self.inputs = {}
        self.outputs = {}
        self.extras = {}
```

```python
class RuleConfig(IdBase):
    type = "CONFIG"

    def __init__(self, rule_id=None):
        super().__init__(rule_id)
        self.name = None
        self.configType = None
        self.inputs = {}
        self.outputs = {}
        self.extras = {}
```

```python
class RuleOnConfig(UiCmd):
    type = "RULE_ON_CFG"

    def __init__(self, rule_id=None):
        super().__init__(rule_id)
        self.scenes = None
        self.timers = None
```

```python
class RuleProposal(metaclass=CozyBase):
    type = "RULE_PROPOSAL"

    def __init__(self):
        super().__init__()
        self.config = None
        self.onConfig = None
```

```python
class RuleData(IdBase):
    type = "RULE"

    def __init__(self, rule_id=None):
        super().__init__(rule_id)
        self.is_on = False
        self.scenes = {}
        self.timers = []
        self.config = None
        self.timestamp = None
```

`RuleConfig` contains the structural configuration of a rule:

`RuleTemplate` is the renderable definition of a rule type. It is returned by `GET /rules/templates` and tells the client which selections and value fields are needed to build a `RuleConfig`.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>TEMPLATE</code></td></tr>
<tr><td><code>category</code></td><td>Template category.</td><td>string</td><td><code>&lt;template-category&gt;</code></td></tr>
<tr><td><code>description</code></td><td>Longer user-facing description of the rule.</td><td>string</td><td><code>Lights on when movement</code></td></tr>
<tr><td><code>summary</code></td><td>Short summary text.</td><td>string</td><td><code>When someone enters a room, the motion sensor detects their movement, and the light is put on.</code></td></tr>
<tr><td><code>configType</code></td><td>Rule type identifier to copy into <code>RuleConfig.configType</code>.</td><td>string</td><td><code>AUTO_LIGHT_RULE</code></td></tr>
<tr><td><code>name</code></td><td>Display name of the template.</td><td>string</td><td><code>Auto light</code></td></tr>
<tr><td><code>priority</code></td><td>Ordering hint for presenting templates and proposals.</td><td>integer or null</td><td><code>100</code></td></tr>
<tr><td><code>icon</code></td><td>Optional icon or visual hint.</td><td>string or null</td><td><code>&lt;template-icon&gt;</code></td></tr>
<tr><td><code>inputs</code></td><td>Mapping from input field name to <code>Input</code> descriptor.</td><td>object</td><td><code>{"motions":{"type":"INPUT",...},"twilights":{"type":"INPUT",...}}</code></td></tr>
<tr><td><code>outputs</code></td><td>Mapping from output field name to <code>Output</code> descriptor.</td><td>object</td><td><code>{"lights":{"type":"OUTPUT",...}}</code></td></tr>
<tr><td><code>extras</code></td><td>Mapping from extra field name to <code>Value</code> descriptor.</td><td>object</td><td><code>{"timeout":{"type":"VALUE",...}}</code></td></tr>
</tbody></table>

The items inside `inputs`, `outputs`, and `extras` are field descriptors. They tell the client what to render and how to validate it.

<table>
<thead><tr><th>Descriptor type</th><th>Serialized <code>type</code></th><th>Explanation</th><th>Important fields</th></tr></thead>
<tbody>
<tr><td><code>Input</code></td><td><code>INPUT</code></td><td>Selects one or more source objects for the rule.</td><td><code>fieldType</code>, <code>capabilities</code>, <code>displayOrder</code>, <code>description</code>, <code>help</code>, <code>minCount</code>, <code>maxCount</code>, <code>defaultValue</code>, <code>readOnly</code>, <code>priority</code>, <code>selectOp</code>, <code>icon</code>, <code>filter</code></td></tr>
<tr><td><code>Output</code></td><td><code>OUTPUT</code></td><td>Selects one or more target objects for the rule.</td><td><code>fieldType</code>, <code>capabilities</code>, <code>displayOrder</code>, <code>description</code>, <code>help</code>, <code>minCount</code>, <code>maxCount</code>, <code>defaultValue</code>, <code>readOnly</code>, <code>priority</code>, <code>selectOp</code>, <code>icon</code>, <code>filter</code></td></tr>
<tr><td><code>Value</code></td><td><code>VALUE</code></td><td>Provides a concrete configurable value instead of an object selection.</td><td><code>fieldType</code>, <code>displayOrder</code>, <code>description</code>, <code>help</code>, <code>minCount</code>, <code>maxCount</code>, <code>minValue</code>, <code>maxValue</code>, <code>defaultValue</code>, <code>options</code>, <code>readOnly</code>, <code>priority</code>, <code>filter</code>, <code>extraFor</code></td></tr>
</tbody></table>

In practice, the client should:

- sort template fields by `displayOrder`
- render `Input` and `Output` fields as object pickers
- filter selectable objects by the field's capability requirements
- enforce `minCount` and `maxCount`
- render `Value` fields according to `fieldType`
- use `defaultValue`, `minValue`, `maxValue`, and `options` as editor constraints
- copy the resulting selections and values into `RuleConfig.inputs`, `RuleConfig.outputs`, and `RuleConfig.extras` using the same field names as keys
- always include every declared input and output field in `RuleConfig`, even when nothing is selected
- represent "nothing selected" for an input or output as an empty array `[]`, not by omitting the field and not by using `null`
- represent "no configured values" for an extra field as an empty array `[]`

Example `AutoLight` template fragment:

```json
{
  "type": "TEMPLATE",
  "configType": "AUTO_LIGHT_RULE",
  "name": "Auto light",
  "description": "Lights on when movement",
  "inputs": {
    "motions": {
      "type": "INPUT",
      "fieldType": "MOTION",
      "capabilities": [],
      "displayOrder": 1,
      "description": "Detects movement",
      "minCount": 1,
      "maxCount": 1000
    },
    "twilights": {
      "type": "INPUT",
      "fieldType": null,
      "capabilities": ["TWILIGHT"],
      "displayOrder": 2,
      "description": "Twilight sensor",
      "minCount": 0
    }
  },
  "outputs": {
    "lights": {
      "type": "OUTPUT",
      "fieldType": "LIGHT",
      "capabilities": ["ON_OFF"],
      "displayOrder": 3,
      "description": "Turn on",
      "minCount": 1
    }
  },
  "extras": {
    "timeout": {
      "type": "VALUE",
      "fieldType": "TIME_DELTA_MS",
      "displayOrder": 4,
      "description": "Turn off after no movement",
      "defaultValue": 300000,
      "minCount": 0,
      "maxCount": 1
    }
  }
}
```

Generated configuration example:

```json
{
  "type": "CONFIG",
  "id": "<rule-uuid>",
  "name": "Hallway auto light",
  "configType": "AUTO_LIGHT_RULE",
  "inputs": {
    "motions": ["<motion-device-uuid>"],
    "twilights": []
  },
  "outputs": {
    "lights": ["<light-device-uuid>"]
  },
  "extras": {
    "timeout": [
      {
        "type": "TIME_DELTA_MS",
        "value": 300000
      }
    ]
  }
}
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>CONFIG</code></td></tr>
<tr><td><code>id</code></td><td>Rule identifier.</td><td>UUID string</td><td><code>&lt;rule-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Rule name.</td><td>string</td><td><code>Hallway auto light</code></td></tr>
<tr><td><code>configType</code></td><td>Rule type identifier.</td><td>string</td><td><code>AUTO_LIGHT_RULE</code></td></tr>
<tr><td><code>inputs</code></td><td>Mapping from every declared input name to referenced object ids. When nothing is selected for a field, the value must be <code>[]</code>.</td><td>object</td><td><code>{"motions":["&lt;motion-device-uuid&gt;"],"twilights":[]}</code></td></tr>
<tr><td><code>outputs</code></td><td>Mapping from every declared output name to referenced object ids. When nothing is selected for a field, the value must be <code>[]</code>.</td><td>object</td><td><code>{"lights":["&lt;light-device-uuid&gt;"]}</code></td></tr>
<tr><td><code>extras</code></td><td>Rule-specific extra values. Each field value is always a list; when no value is configured, use <code>[]</code>.</td><td>object</td><td><code>{"timeout":[{"type":"TIME_DELTA_MS","value":300000}]}</code></td></tr>
</tbody></table>

#### Rule command objects

The simple write-side rule command objects are:

```python
class RuleOnCommand(UiCmd):
    type = "RULE_ON_COMMAND"


class RuleOffCommand(UiCmd):
    type = "RULE_OFF_COMMAND"
```

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload shape</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>RULE_ON_COMMAND</code></td><td>Turns the target rule on.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "RULE_ON_COMMAND",
  "id": "&lt;rule-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>RULE_OFF_COMMAND</code></td><td>Turns the target rule off.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "RULE_OFF_COMMAND",
  "id": "&lt;rule-uuid&gt;"
}</code></pre></td></tr>
</tbody></table>

`RuleOnConfig` defines when a rule is on:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>RULE_ON_CFG</code></td></tr>
<tr><td><code>id</code></td><td>Target rule id.</td><td>UUID string or null</td><td><code>&lt;rule-uuid&gt;</code></td></tr>
<tr><td><code>scenes</code></td><td>List of scene ids. The rule is on when at least one of these scenes is on.</td><td>array of strings</td><td><code>["&lt;scene-uuid&gt;"]</code></td></tr>
<tr><td><code>timers</code></td><td>Timer definitions telling when the rule is on.</td><td>array</td><td><code>[{"type":"RULETIME",...}]</code></td></tr>
</tbody></table>

A manual-only configuration can be serialized as:

```json
{
  "type": "RULE_ON_CFG",
  "scenes": [],
  "timers": []
}
```

With empty `scenes` and `timers`, the rule is not activated by scenes or schedules and is instead controlled directly with `RULE_ON_COMMAND` and `RULE_OFF_COMMAND`.

#### Rule read objects

`RuleData` is the runtime or persisted rule object returned by the Hub.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>RULE</code></td></tr>
<tr><td><code>id</code></td><td>Rule identifier.</td><td>UUID string</td><td><code>&lt;rule-uuid&gt;</code></td></tr>
<tr><td><code>is_on</code></td><td>Whether the rule is currently on.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>scenes</code></td><td>Mapping from scene id to current on/off state for scenes affecting the rule.</td><td>object</td><td><code>{"&lt;scene-uuid&gt;":true}</code></td></tr>
<tr><td><code>timers</code></td><td>List of active timer definitions.</td><td>array</td><td><code>[]</code></td></tr>
<tr><td><code>config</code></td><td>Nested <code>RuleConfig</code>.</td><td>object</td><td><code>{"type":"CONFIG",...}</code></td></tr>
<tr><td><code>timestamp</code></td><td>Polling-helper timestamp used for incremental synchronization. Treat as read-only metadata; do not use it as application data and do not send it in commands.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
</tbody></table>

`RuleProposal` is used by the proposal endpoint.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>RULE_PROPOSAL</code></td></tr>
<tr><td><code>config</code></td><td>Proposed <code>RuleConfig</code>.</td><td>object</td><td><code>{"type":"CONFIG",...}</code></td></tr>
<tr><td><code>onConfig</code></td><td>Proposed <code>RuleOnConfig</code>.</td><td>object</td><td><pre><code class="language-json">{
  "type": "RULE_ON_CFG",
  "id": "&lt;rule-uuid&gt;",
  "scenes": [],
  "timers": []
}</code></pre></td></tr>
</tbody></table>

#### Typed rule value objects

Rule templates use `Value.fieldType` to describe the kind of editor the client should render. When the client writes the chosen value back into `RuleConfig.extras`, each stored value is serialized as a concrete typed object.

```python
class TimeDelta:
    type = "TIME_DELTA_MS"

    def __init__(self, time_ms=-1):
        self.value = time_ms


class FreeText:
    type = "FREE_TEXT"

    def __init__(self, text=""):
        self.text = text


class Password:
    type = "PASSWORD"

    def __init__(self, value=None):
        self.value = value


class EmailAddress:
    type = "EMAIL"

    def __init__(self, email=""):
        self.value = email


class NumericValue:
    type = "NUMBER"

    def __init__(self, value):
        self.value = value


class PercentValue:
    type = "PERCENT"

    def __init__(self, value):
        self.value = value


class CelsiusValue:
    type = "CELSIUS"

    def __init__(self, value):
        self.value = value


class PpmValue:
    type = "PPM"

    def __init__(self, value):
        self.value = value


class BooleanValue:
    type = "BOOL"

    def __init__(self, value):
        self.value = value


class OptionValue:
    type = "OPTION"

    def __init__(self, value):
        self.value = value


class ButtonIdValue:
    type = "BUTTON_ID"

    def __init__(self, value):
        self.rc = None
        self.value = value


class SelectionValue:
    type = "SELECTION"

    def __init__(self, value):
        self.value = value


class TimedPreset:
    type = "TIMED_PRESET"

    def __init__(self):
        self.timer = None
        self.state = None


class UserNotification:
    type = "USER_NOTIFICATION"

    def __init__(self):
        self.userId = None
        self.sendSms = None
        self.sendEmail = None
        self.sendNotification = None
        self.extraEmail = None
        self.extraSms = None
```

`RuleConfig.extras[fieldName]` is always a list of these typed objects, even when the template allows only one value.

<table>
<thead><tr><th>Serialized type</th><th>Fields</th><th>Meaning</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>TIME_DELTA_MS</code></td><td><code>value</code></td><td>Timeout or delay in milliseconds.</td><td><code>{"type":"TIME_DELTA_MS","value":300000}</code></td></tr>
<tr><td><code>FREE_TEXT</code></td><td><code>text</code></td><td>Free text value.</td><td><code>{"type":"FREE_TEXT","text":"Night mode"}</code></td></tr>
<tr><td><code>PASSWORD</code></td><td><code>value</code></td><td>Password-like string value.</td><td><code>{"type":"PASSWORD","value":"secret"}</code></td></tr>
<tr><td><code>EMAIL</code></td><td><code>value</code></td><td>Email address value.</td><td><code>{"type":"EMAIL","value":"alerts@example.com"}</code></td></tr>
<tr><td><code>NUMBER</code></td><td><code>value</code></td><td>Generic numeric value.</td><td><code>{"type":"NUMBER","value":42}</code></td></tr>
<tr><td><code>PERCENT</code></td><td><code>value</code></td><td>Percentage value.</td><td><code>{"type":"PERCENT","value":80}</code></td></tr>
<tr><td><code>CELSIUS</code></td><td><code>value</code></td><td>Temperature in Celsius.</td><td><code>{"type":"CELSIUS","value":21.5}</code></td></tr>
<tr><td><code>PPM</code></td><td><code>value</code></td><td>Parts-per-million value.</td><td><code>{"type":"PPM","value":900}</code></td></tr>
<tr><td><code>BOOL</code></td><td><code>value</code></td><td>Boolean value.</td><td><code>{"type":"BOOL","value":true}</code></td></tr>
<tr><td><code>OPTION</code></td><td><code>value</code></td><td>Selected option value.</td><td><code>{"type":"OPTION","value":true}</code></td></tr>
<tr><td><code>BUTTON_ID</code></td><td><code>value</code>, <code>rc</code></td><td>Remote-control or wall-switch button selection.</td><td><code>{"type":"BUTTON_ID","rc":"&lt;device-uuid&gt;","value":1}</code></td></tr>
<tr><td><code>SELECTION</code></td><td><code>value</code></td><td>Selection value from a predefined set.</td><td><code>{"type":"SELECTION","value":"phase1"}</code></td></tr>
<tr><td><code>RULETIME</code></td><td><code>cronDef</code>, <code>enabled</code></td><td>Rule-time schedule object used directly in timer-based extras.</td><td><pre><code class="language-json">{
  "type": "RULETIME",
  "cronDef": {
    "type": "CRONDEF",
    "start": "0 18 * * *",
    "stop": "0 23 * * *"
  },
  "enabled": true
}</code></pre></td></tr>
<tr><td><code>TIMED_PRESET</code></td><td><code>timer</code>, <code>state</code></td><td>Preset to be applied at a specific rule time.</td><td><pre><code class="language-json">{
  "type": "TIMED_PRESET",
  "timer": {
    "type": "RULETIME",
    "cronDef": {
      "type": "CRONDEF",
      "start": "0 18 * * *",
      "stop": "0 23 * * *"
    },
    "enabled": true
  },
  "state": {
    "type": "STATE_LIGHT",
    "isOn": true,
    "brightness": 0.8
  }
}</code></pre></td></tr>
<tr><td><code>USER_NOTIFICATION</code></td><td><code>userId</code>, <code>sendSms</code>, <code>sendEmail</code>, <code>sendNotification</code>, <code>extraEmail</code>, <code>extraSms</code></td><td>User-notification target and channel selection.</td><td><code>{"type":"USER_NOTIFICATION","userId":"&lt;user-uuid&gt;","sendNotification":true}</code></td></tr>
</tbody></table>

Template `defaultValue` is not always serialized in the same shape as the stored value object. For example, a timeout template usually exposes raw `defaultValue: 300000`, but the corresponding stored rule extra is `{"type":"TIME_DELTA_MS","value":300000}`.

`BUTTON_ID` always binds a button number to one concrete source device through `rc`. The referenced device is the same remote-control or wall-switch style button device that the rule input selects. When a template contains two `BUTTON_ID` fields that form the conventional on/off pair, both serialized values use the same `rc`; the first button is the logical "on" button and the second is the matching "off" button with button id `on + 1`.

The timer model used by rules and scenes is built from:

```python
class CronDef(object):
    type = "CRONDEF"

    def __init__(self, start=None, stop=None):
        super().__init__()
        self.start = start
        self.stop = stop
```

```python
class RuleTiming(object):
    type = "RULETIME"

    def __init__(self, crondef=None):
        super().__init__()
        self.cronDef = crondef
        self.enabled = True
```

<table>
<thead><tr><th>Object</th><th>Serialized <code>type</code></th><th>Explanation</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CronDef</code></td><td><code>CRONDEF</code></td><td>Repeating start or stop definition in cron-like string format.</td><td><pre><code class="language-json">{
  "type": "CRONDEF",
  "start": "0 18 * * *",
  "stop": "0 23 * * *"
}</code></pre></td></tr>
<tr><td><code>RuleTiming</code></td><td><code>RULETIME</code></td><td>Timer wrapper containing <code>cronDef</code> and <code>enabled</code>.</td><td><pre><code class="language-json">{
  "type": "RULETIME",
  "cronDef": {
    "type": "CRONDEF",
    "start": "0 18 * * *",
    "stop": "0 23 * * *"
  },
  "enabled": true
}</code></pre></td></tr>
</tbody></table>

Example:

```json
{
  "type": "RULE",
  "id": "<rule-uuid>",
  "is_on": true,
  "scenes": {
    "<scene-uuid>": true
  },
  "timers": [],
  "config": {
    "type": "CONFIG",
    "id": "<rule-uuid>",
    "name": "Hallway auto light",
    "configType": "AUTO_LIGHT_RULE",
    "inputs": {
      "motions": ["<motion-device-uuid>"],
      "twilights": ["<twilight-device-uuid>"]
    },
    "outputs": {
      "lights": ["<light-device-uuid>"]
    },
    "extras": {
      "timeout": [
        {
          "type": "TIME_DELTA_MS",
          "value": 300000
        }
      ]
    }
  },
  "timestamp": 1710249600000
}
```

### 6.11 Scene data

The shared scene object is `Scene`.

Resource layout:

- read objects: [Scene read objects](#scene-read-objects) returned by `GET /scenes`
- write commands: [Scene command objects](#scene-command-objects) used by `PUT /scenes` and `PUT /scenes/command`
- examples: object examples in this section and endpoint examples in [5.5 Scene endpoints](#55-scene-endpoints)

#### Scene read objects

```python
class Scene(IdBase):
    type = "SCENE"

    def __init__(self, scene_id=None, category="USER", name=""):
        super().__init__(scene_id)
        self.category = category
        self.name = name
        self.isOn = False
        self.icon = None
        self.presets = {}
        self.excludedIds = []
        self.sceneTimes = []
        self.timestamp = None
        self.requiredIds = []
```

```python
class Preset(IdBase):
    type = "PRESET"

    def __init__(self, preset_id=None):
        super().__init__(preset_id)
        self.targetIds = None
        self.state = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>SCENE</code></td></tr>
<tr><td><code>id</code></td><td>Scene identifier.</td><td>UUID string</td><td><code>&lt;scene-uuid&gt;</code></td></tr>
<tr><td><code>category</code></td><td>User-created or built-in scene category.</td><td>string</td><td><code>USER</code></td></tr>
<tr><td><code>name</code></td><td>Scene name.</td><td>string</td><td><code>Evening</code></td></tr>
<tr><td><code>isOn</code></td><td>Whether the scene is currently on.</td><td>boolean</td><td><code>false</code></td></tr>
<tr><td><code>icon</code></td><td>Rendering hint used mainly for factory scenes.</td><td>string or null</td><td><code>null</code></td></tr>
<tr><td><code>presets</code></td><td>Mapping from preset id to preset data.</td><td>object</td><td><code>{"&lt;preset-uuid&gt;":{"type":"PRESET",...}}</code></td></tr>
<tr><td><code>excludedIds</code></td><td>Scene ids that cannot be on at the same time.</td><td>array of strings</td><td><code>[]</code></td></tr>
<tr><td><code>sceneTimes</code></td><td>Timer definitions controlling automatic on/off behavior.</td><td>array</td><td><code>[{"type":"RULETIME",...}]</code></td></tr>
<tr><td><code>timestamp</code></td><td>Polling-helper timestamp used for incremental synchronization. Treat as read-only metadata; do not use it as application data and do not send it in commands.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>requiredIds</code></td><td>Prerequisite scene ids.</td><td>array of strings</td><td><code>[]</code></td></tr>
</tbody></table>

`Preset` is the object used inside `Scene.presets`.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>PRESET</code></td></tr>
<tr><td><code>id</code></td><td>Preset identifier.</td><td>UUID string</td><td><code>&lt;preset-uuid&gt;</code></td></tr>
<tr><td><code>targetIds</code></td><td>Target device ids.</td><td>array of strings</td><td><code>["&lt;device-uuid&gt;"]</code></td></tr>
<tr><td><code>state</code></td><td>Device state to apply when the scene is turned on.</td><td>object</td><td><code>{"type":"STATE_LIGHT","isOn":true}</code></td></tr>
</tbody></table>

#### Scene command objects

Scene write operations use three command object families.

```python
class ChangeScene(UiCmd):
    type = "CMD_SCENE"

    def __init__(self, scene_id=None):
        super().__init__(scene_id)
        self.name = None
        self.isOn = None
        self.presets = None
        self.sceneTimes = None
```

```python
class SceneOnCommand(UiCmd):
    type = "CMD_SCENE_ON"
```

```python
class SceneOffCommand(UiCmd):
    type = "CMD_SCENE_OFF"
```

`ChangeScene` is the create/update command used by `PUT /scenes`. To create a new scene, set <code>id</code> to <code>null</code>. Only documented mutable fields are shown below; omitted or <code>null</code> fields are left unchanged.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_SCENE</code></td></tr>
<tr><td><code>id</code></td><td>Target scene id, or <code>null</code> when creating a new scene.</td><td>UUID string or null</td><td><code>&lt;scene-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Scene name.</td><td>string or null</td><td><code>Evening</code></td></tr>
<tr><td><code>isOn</code></td><td>Current on/off state.</td><td>boolean or null</td><td><code>false</code></td></tr>
<tr><td><code>presets</code></td><td>Scene preset mapping.</td><td>object or null</td><td><code>{"&lt;preset-uuid&gt;":{"type":"PRESET",...}}</code></td></tr>
<tr><td><code>sceneTimes</code></td><td>Timer list for automatic on/off scheduling.</td><td>array or null</td><td><code>[{"type":"RULETIME",...}]</code></td></tr>
</tbody></table>

The dedicated on/off commands used by `PUT /scenes/command` are:

<table>
<thead><tr><th>Command type</th><th>Explanation</th><th>Payload shape</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>CMD_SCENE_ON</code></td><td>Turns the target scene on.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_SCENE_ON",
  "id": "&lt;scene-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>CMD_SCENE_OFF</code></td><td>Turns the target scene off.</td><td><code>{type, id}</code></td><td><pre><code class="language-json">{
  "type": "CMD_SCENE_OFF",
  "id": "&lt;scene-uuid&gt;"
}</code></pre></td></tr>
</tbody></table>

Important scene semantics:

- `category=FACTORY` identifies built-in scenes such as the default Home and Away scenes
- factory scenes cannot be renamed or deleted
- `sceneTimes` allows a scene to be turned on and off automatically by time
- `excludedIds` expresses mutual exclusion between scenes
- `requiredIds` exists in the data model even though it is not part of the normal UI workflow

Example:

```json
{
  "type": "SCENE",
  "id": "<scene-uuid>",
  "category": "USER",
  "name": "Evening",
  "isOn": false,
  "icon": null,
  "presets": {
    "<preset-uuid>": {
      "type": "PRESET",
      "id": "<preset-uuid>",
      "targetIds": ["<device-uuid>"],
      "state": {
        "type": "STATE_LIGHT",
        "isOn": true,
        "brightness": 0.3
      }
    }
  },
  "excludedIds": [],
  "sceneTimes": [
    {
      "type": "RULETIME",
      "cronDef": {
        "type": "CRONDEF",
        "start": "0 18 * * *",
        "stop": "0 23 * * *"
      },
      "enabled": true
    }
  ],
  "timestamp": 1710249600000,
  "requiredIds": []
}
```

### 6.12 Room data

The shared room object is `Room`.

Resource layout:

- read objects: [Room read objects](#room-read-objects) returned by `GET /rooms`
- write commands: [Room command objects](#room-command-objects) used by `PUT /rooms` and `PUT /rooms/order`
- examples: object examples in this section and endpoint examples in [5.6 Room endpoints](#56-room-endpoints)

#### Room read objects

```python
class RoomStatus(IdBase):
    type = "ROOM_STATUS"

    def __init__(self, room_id=None):
        super().__init__(room_id)
```

```python
class Room(IdBase):
    type = "ROOM"

    def __init__(self, room_id=None):
        super().__init__(room_id)
        self.name = None
        self.order = None
        self.status = RoomStatus(room_id)
        self.timestamp = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>ROOM</code></td></tr>
<tr><td><code>id</code></td><td>Room identifier.</td><td>UUID string</td><td><code>&lt;room-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Room name.</td><td>string</td><td><code>Living room</code></td></tr>
<tr><td><code>order</code></td><td>Ordering index for UI presentation.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>status</code></td><td>Nested <code>RoomStatus</code>.</td><td>object</td><td><code>{"type":"ROOM_STATUS","id":"&lt;room-uuid&gt;"}</code></td></tr>
<tr><td><code>timestamp</code></td><td>Last update timestamp.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
</tbody></table>

`RoomStatus` currently contains only the room id and acts mainly as a typed nested status object with `type="ROOM_STATUS"`.

The `order` field is important because room ordering is managed explicitly through `/rooms/order`.

#### Room command objects

Room write operations use one general create/update command and one ordering command.

```python
class RoomCommand(UiCmd):
    type = "CMD_ROOM"

    def __init__(self, room_id=None):
        super().__init__(room_id)
        self.name = None
```

```python
class SetRoomOrder(UiCmd):
    type = "CMD_SET_ROOM_ORDER"

    def __init__(self, room_id=None):
        super().__init__(room_id)
        self.order = None
```

`RoomCommand` is used by `PUT /rooms`. To create a new room, set <code>id</code> to <code>null</code>.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_ROOM</code></td></tr>
<tr><td><code>id</code></td><td>Target room id, or <code>null</code> when creating a new room.</td><td>UUID string or null</td><td><code>&lt;room-uuid&gt;</code></td></tr>
<tr><td><code>name</code></td><td>Replacement room name.</td><td>string</td><td><code>Living room</code></td></tr>
</tbody></table>

`SetRoomOrder` is used by `PUT /rooms/order`.

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CMD_SET_ROOM_ORDER</code></td></tr>
<tr><td><code>id</code></td><td>Target room id.</td><td>UUID string</td><td><code>&lt;room-uuid&gt;</code></td></tr>
<tr><td><code>order</code></td><td>Replacement ordering index.</td><td>integer</td><td><code>0</code></td></tr>
</tbody></table>

Example:

```json
{
  "type": "ROOM",
  "id": "<room-uuid>",
  "name": "Living room",
  "order": 0,
  "status": {
    "type": "ROOM_STATUS",
    "id": "<room-uuid>"
  },
  "timestamp": 1710249600000
}
```

### 6.13 Alerts and alarms

The API uses both alerts and alarms. They are related but not the same thing.

Alerts are lightweight user-facing notices returned through poll data:

- delta type: `UserAlerts`
- item type: `UserAlert`

```python
class UserAlert(IdBase, Message):
    type = "USER_ALERT"

    def __init__(self, msg_id=None, source_id=None, error=False, realtime_ms=None, **kwargs):
        super().__init__(IdBase.generate_id())
        self.msg_id = msg_id
        self.sourceId = source_id
        self.message = None
        self.realtime_ms = realtime_ms
        self.error = error
        self.userId = None
        self.__kwargs = kwargs
```

`UserAlert` contains:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>USER_ALERT</code></td></tr>
<tr><td><code>id</code></td><td>Generated alert id.</td><td>string</td><td><code>&lt;alert-id&gt;</code></td></tr>
<tr><td><code>msg_id</code></td><td>Alert code.</td><td>string</td><td><code>RULE_DISABLED</code></td></tr>
<tr><td><code>sourceId</code></td><td>Originating object id when available.</td><td>string or null</td><td><code>&lt;rule-uuid&gt;</code></td></tr>
<tr><td><code>message</code></td><td>Translated user-facing text.</td><td>string</td><td><code>Rule has been disabled.</code></td></tr>
<tr><td><code>realtime_ms</code></td><td>Creation time.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>error</code></td><td>Severity flag.</td><td>boolean</td><td><code>false</code></td></tr>
<tr><td><code>userId</code></td><td>Optional user id.</td><td>string or null</td><td><code>null</code></td></tr>
</tbody></table>

Alerts are mainly transient UI notifications.

Alarms are tracked alarm objects returned by `/alarms` and alarm deltas:

- delta type: `AlarmDelta`
- item type: `UserAlarm`

```python
class UserAlarm(IdBase, Message):
    type = "USER_ALARM"

    def __init__(self, alarm_id=None, source_id=None, error="err", **kwargs):
        super().__init__(IdBase.generate_id() if not alarm_id else alarm_id)
        self.sourceId = source_id
        self.title = None
        self.name = None
        self.message = None
        self.timestamp = None
        self.createdAtMs = None
        self.level = error
        self.closed = None
        self.kwargs = kwargs
```

`UserAlarm` contains:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>USER_ALARM</code></td></tr>
<tr><td><code>id</code></td><td>Alarm identifier.</td><td>string</td><td><code>&lt;alarm-id&gt;</code></td></tr>
<tr><td><code>sourceId</code></td><td>Originating object id when available.</td><td>string or null</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>title</code></td><td>Localized title.</td><td>object</td><td><code>{"":"Test alert"}</code></td></tr>
<tr><td><code>name</code></td><td>Alarm name.</td><td>string</td><td><code>Test alarm</code></td></tr>
<tr><td><code>message</code></td><td>Localized or HTML-formatted message content.</td><td>object</td><td><code>{"":"&lt;div&gt;Test alarm content&lt;/div&gt;"}</code></td></tr>
<tr><td><code>timestamp</code></td><td>Alarm timestamp.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>createdAtMs</code></td><td>Creation time.</td><td>timestampms</td><td><code>1710249600000</code></td></tr>
<tr><td><code>level</code></td><td>Severity level.</td><td>string</td><td><code>err</code></td></tr>
<tr><td><code>closed</code></td><td>Whether the alarm has been closed.</td><td>boolean</td><td><code>false</code></td></tr>
<tr><td><code>kwargs</code></td><td>Formatting data used when building the message.</td><td>object or null</td><td><code>null</code></td></tr>
</tbody></table>

In practice:

- alerts are lightweight notices for polling and UI messaging
- alarms are persistent tracked alarm records with close/delete operations

Example alert:

```json
{
  "type": "USER_ALERT",
  "id": "<alert-id>",
  "msg_id": "RULE_DISABLED",
  "sourceId": "<rule-uuid>",
  "message": "Rule has been disabled.",
  "realtime_ms": 1710249600000,
  "error": false,
  "userId": null
}
```

Example alarm:

```json
{
  "type": "USER_ALARM",
  "id": "<alarm-id>",
  "sourceId": "<device-uuid>",
  "title": {
    "": "Test alert"
  },
  "name": "Test alarm",
  "message": {
    "": "<div>Test alarm content</div>"
  },
  "timestamp": 1710249600000,
  "createdAtMs": 1710249600000,
  "level": "err",
  "closed": false
}
```

### 6.14 Zigbee network data

The Zigbee-related Hub endpoints return and accept explicit Zigbee network objects.

```python
class ZigbeeRadioStats(Message):
    type = "ZIGBEE_RADIO_STATS"

    def __init__(self):
        super().__init__()
        self.rx_successful = None
        self.rx_err_none = None
        self.rx_err_invalid_frame = None
        self.rx_err_invalid_fcs = None
        self.rx_err_invalid_dest_addr = None
        self.rx_err_runtime = None
        self.rx_err_timeslot_ended = None
        self.rx_err_aborted = None
        self.tx_successful = None
        self.tx_err_none = None
        self.tx_err_busy_channel = None
        self.tx_err_invalid_ack = None
        self.tx_err_no_mem = None
        self.tx_err_timeslot_ended = None
        self.tx_err_no_ack = None
        self.tx_err_aborted = None
        self.tx_err_timeslot_denied = None
```

```python
class ZigbeeNetworkStatus(Message):
    type = "ZIGBEE_NETWORK_STATUS"

    def __init__(self, connected, has_network=None, channel=None, power=None, firmware=None, stats=None,
                 ignore_reachability=None):
        super().__init__()
        self.connected = connected
        self.has_network = has_network
        self.channel = channel
        self.power = power
        self.firmware = firmware
        self.stats = stats
        self.ignore_reachability = ignore_reachability
```

```python
class ZigbeeNetworkSettings(AskMessage):
    type = "ZIGBEE_NETWORK_SETTINGS"

    def __init__(self, channel=None, power=None, ignore_reachability=None):
        super().__init__()
        self.channel = channel
        self.power = power
        self.ignore_reachability = ignore_reachability
```

`ZigbeeRadioStats` contains radio-level receive and transmit counters:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>ZIGBEE_RADIO_STATS</code></td></tr>
<tr><td><code>rx_successful</code></td><td>Count of successful receive operations.</td><td>integer</td><td><code>1250</code></td></tr>
<tr><td><code>rx_err_none</code></td><td>Receive operations reported without an error condition.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_invalid_frame</code></td><td>Received frames rejected as invalid.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_invalid_fcs</code></td><td>Received frames with invalid frame check sequence.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_invalid_dest_addr</code></td><td>Received frames with an invalid destination address.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_runtime</code></td><td>Receive runtime errors.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_timeslot_ended</code></td><td>Receive operations interrupted because the timeslot ended.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>rx_err_aborted</code></td><td>Aborted receive operations.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_successful</code></td><td>Count of successful transmit operations.</td><td>integer</td><td><code>980</code></td></tr>
<tr><td><code>tx_err_none</code></td><td>Transmit operations reported without an error condition.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_err_busy_channel</code></td><td>Transmit operations blocked by a busy channel.</td><td>integer</td><td><code>2</code></td></tr>
<tr><td><code>tx_err_invalid_ack</code></td><td>Transmit operations that received an invalid acknowledgement.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_err_no_mem</code></td><td>Transmit operations that failed because of missing memory.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_err_timeslot_ended</code></td><td>Transmit operations interrupted because the timeslot ended.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_err_no_ack</code></td><td>Transmit operations that did not receive an acknowledgement.</td><td>integer</td><td><code>1</code></td></tr>
<tr><td><code>tx_err_aborted</code></td><td>Aborted transmit operations.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>tx_err_timeslot_denied</code></td><td>Transmit operations denied because no timeslot was granted.</td><td>integer</td><td><code>0</code></td></tr>
</tbody></table>

`ZigbeeNetworkStatus` describes the current Zigbee network state:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>ZIGBEE_NETWORK_STATUS</code></td></tr>
<tr><td><code>connected</code></td><td>Whether the Zigbee controller is connected.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>has_network</code></td><td>Whether the controller currently has an active Zigbee network.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>channel</code></td><td>Current Zigbee channel.</td><td>integer</td><td><code>15</code></td></tr>
<tr><td><code>power</code></td><td>Current Zigbee radio power.</td><td>integer</td><td><code>8</code></td></tr>
<tr><td><code>firmware</code></td><td>Zigbee controller firmware identifier.</td><td>string</td><td><code>6.10.3</code></td></tr>
<tr><td><code>stats</code></td><td>Nested Zigbee radio statistics.</td><td>object</td><td><code>{"type":"ZIGBEE_RADIO_STATS","rx_successful":1250,"tx_successful":980}</code></td></tr>
<tr><td><code>ignore_reachability</code></td><td>Whether automation commands are sent even when devices are currently marked unreachable.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

`ZigbeeNetworkSettings` is the request object used for updating Zigbee network settings:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this request object.</td><td>string</td><td><code>ZIGBEE_NETWORK_SETTINGS</code></td></tr>
<tr><td><code>channel</code></td><td>Target Zigbee channel.</td><td>integer</td><td><code>15</code></td></tr>
<tr><td><code>power</code></td><td>Target Zigbee radio power.</td><td>integer</td><td><code>8</code></td></tr>
<tr><td><code>ignore_reachability</code></td><td>Whether automation commands should ignore reachability state.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

Example Zigbee status object:

```json
{
  "type": "ZIGBEE_NETWORK_STATUS",
  "connected": true,
  "has_network": true,
  "channel": 15,
  "power": 8,
  "firmware": "6.10.3",
  "stats": {
    "type": "ZIGBEE_RADIO_STATS",
    "rx_successful": 1250,
    "rx_err_none": 0,
    "rx_err_invalid_frame": 0,
    "rx_err_invalid_fcs": 0,
    "rx_err_invalid_dest_addr": 0,
    "rx_err_runtime": 0,
    "rx_err_timeslot_ended": 0,
    "rx_err_aborted": 0,
    "tx_successful": 980,
    "tx_err_none": 0,
    "tx_err_busy_channel": 2,
    "tx_err_invalid_ack": 0,
    "tx_err_no_mem": 0,
    "tx_err_timeslot_ended": 0,
    "tx_err_no_ack": 1,
    "tx_err_aborted": 0,
    "tx_err_timeslot_denied": 0
  },
  "ignore_reachability": false
}
```


## Part 3 - Protocol operations and integration-specific chapters

## 7. BACnet operations

This chapter collects the BACnet-specific workflow guidance and BACnet data objects used by the endpoints in section `5.8`.

### 7.1 BACnet functionality

The BACnet API is used to manage the Hub's BACnet integration.

This includes:

- reading and changing the Hub's BACnet server settings
- restoring default BACnet settings
- listing available local BACnet device classes
- listing supported BACnet object types
- creating local BACnet devices exposed by the Hub
- scanning external BACnet devices
- reading scan results
- pairing external BACnet objects into the Hub

All BACnet endpoints require user-admin access.

BACnet device discovery and pairing are used while pairing is active.

The main BACnet workflow starts by reading or updating settings:

- `GET /bacnet/settings` reads the current BACnet settings
- `POST /bacnet/settings` saves BACnet settings
- `POST /bacnet/defaultsettings` restores the default BACnet settings

BACnet settings include at least:

- whether BACnet server functionality is enabled
- the Hub BACnet device id
- the Hub BACnet device name
- the BACnet port
- broadcast mode

These settings apply only to the Hub's BACnet server role. BACnet client functionality such as scanning and pairing is
not configured through `BACNET_SETTINGS`.

For local BACnet objects exposed by the Hub, the available helper endpoints are:

- `GET /bacnet/localdevicetypes`
- `GET /bacnet/objecttypes`
- `POST /bacnet/device`

`GET /bacnet/localdevicetypes` returns the available local BACnet device types that the Hub can create.

`GET /bacnet/objecttypes` returns the supported BACnet object types.

`POST /bacnet/device` creates a local BACnet device or object published by the Hub. The request identifies the local device type to create.

The request model also contains `objectId` and `objectName` fields, but they are currently not used by the implementation.

For external BACnet devices, the workflow is:

1. Start a scan with `POST /bacnet/scan`.
2. Read available scan results with `GET /bacnet/devices`.
3. Pair the selected BACnet object with `POST /bacnet/pair`.

The scan request can target a BACnet device by address and device id, and it can also restrict scanning to a specific object type and object id.

`GET /bacnet/devices` returns the discovered BACnet devices and their available objects collected by the scan.

Pairing an external BACnet object uses:

- BACnet device id
- object type
- object id
- capability, which selects the type of Hub device to create

#### Example sequence: BACnet discovery and pairing during normal device pairing

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/hub/scan?ts=0
    Hub-->>Client: SCAN_DELTA {full=true, devices=[...]}
    Note over Hub: Normal device scanning starts
    Client->>Hub: POST /cc/<api-version>/bacnet/scan\nSCAN_BACNET_DEVICE
    Hub-->>Client: Scan accepted
    par Normal scan polling
        loop While pairing is active
            Client->>Hub: GET /cc/<api-version>/hub/scan?ts=<last-scan-ts>
            Hub-->>Client: SCAN_DELTA {devices=[DEVICE_SCAN, ...]}
        end
    and BACnet scan polling
        loop While pairing is active
            Client->>Hub: GET /cc/<api-version>/bacnet/devices
            Hub-->>Client: {identifier: BACNET_HOST, ...}
        end
    end
    Note over Client: Client UI needs separate BACnet pairing functionality while normal pairing is active
    User->>Client: Select BACnet device to pair
    Client->>Hub: POST /cc/<api-version>/bacnet/pair\nPAIR_BACNET_DEVICE {deviceId, objectType, objectId, capability}
    Hub-->>Client: Paired device data
```

### 7.2 BACnet data

The BACnet API uses a small set of explicit BACnet data objects.

```python
class BacnetSettings(AskMessage):
    type = "BACNET_SETTINGS"

    def __init__(self):
        super().__init__()
        self.bacnetEnabled = None
        self.deviceId = None
        self.deviceName = None
        self.port = None
        self.broadcast = None
```

```python
class BacnetDeviceId(object):
    type = "BACNET_ID"

    def __init__(self):
        super().__init__()
        self.deviceId = None
        self.objectType = None
        self.objectId = None
        self.objectName = None
        self.address = None
        self.capability = None
        self.locked = None
```

```python
class ScanBacnetDevice(AskMessage):
    type = "SCAN_BACNET_DEVICE"

    def __init__(self):
        super().__init__()
        self.address = None
        self.deviceId = None
        self.objectType = None
        self.objectId = None
```

```python
class CreateLocalBacnetDevice(AskMessage):
    type = "CREATE_BACNET_DEVICE"

    def __init__(self):
        super().__init__()
        self.id = None
        self.objectId = None
        self.objectName = None
```

```python
class PairBacnetDevice(AskMessage):
    type = "PAIR_BACNET_DEVICE"

    def __init__(self):
        super().__init__()
        self.deviceId = None
        self.objectType = None
        self.objectId = None
        self.capability = None
```

`BacnetSettings` contains the Hub BACnet server configuration:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>BACNET_SETTINGS</code></td></tr>
<tr><td><code>bacnetEnabled</code></td><td>Whether the Hub's BACnet server functionality is enabled.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>deviceId</code></td><td>BACnet device id used by the Hub.</td><td>integer</td><td><code>12345</code></td></tr>
<tr><td><code>deviceName</code></td><td>BACnet device name used by the Hub.</td><td>string</td><td><code>Cozify Hub</code></td></tr>
<tr><td><code>port</code></td><td>BACnet UDP port.</td><td>integer</td><td><code>47808</code></td></tr>
<tr><td><code>broadcast</code></td><td>Whether general broadcast is used.</td><td>boolean</td><td><code>true</code></td></tr>
</tbody></table>

Handling rules:

- `bacnetEnabled` controls the Hub's BACnet server configuration state only
- changing `deviceId`, `deviceName`, `port`, or `broadcast` restarts the BACnet integration so the new settings take effect
- `deviceId` must be a positive integer in the valid BACnet device-id range accepted by the implementation
- `deviceName` must be a non-empty string
- `port` must be `>= 1025`
- `broadcast` must be boolean when supplied
- restoring default settings resets `port` to `47808`, resets `broadcast` to `true`, and regenerates the default `deviceId` and `deviceName`

`BacnetDeviceId` identifies a BACnet object:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this object.</td><td>string</td><td><code>BACNET_ID</code></td></tr>
<tr><td><code>deviceId</code></td><td>BACnet device id.</td><td>string or integer</td><td><code>400001</code></td></tr>
<tr><td><code>objectType</code></td><td>BACnet object type.</td><td>string</td><td><code>analogInput</code></td></tr>
<tr><td><code>objectId</code></td><td>BACnet object instance number.</td><td>integer</td><td><code>7</code></td></tr>
<tr><td><code>objectName</code></td><td>BACnet object name.</td><td>string</td><td><code>Room temperature</code></td></tr>
<tr><td><code>address</code></td><td>IP address for external BACnet devices.</td><td>string</td><td><code>192.168.1.50</code></td></tr>
<tr><td><code>capability</code></td><td>Hub capability represented by the BACnet object.</td><td>string</td><td><code>TEMPERATURE</code></td></tr>
<tr><td><code>locked</code></td><td>Whether BACnet-side metadata is locked against automatic renaming.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

`ScanBacnetDevice` is the scan request object:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this request object.</td><td>string</td><td><code>SCAN_BACNET_DEVICE</code></td></tr>
<tr><td><code>address</code></td><td>BACnet device IP address.</td><td>string</td><td><code>192.168.1.50</code></td></tr>
<tr><td><code>deviceId</code></td><td>BACnet device id.</td><td>integer</td><td><code>400001</code></td></tr>
<tr><td><code>objectType</code></td><td>Optional object type filter.</td><td>string or null</td><td><code>analogInput</code></td></tr>
<tr><td><code>objectId</code></td><td>Optional object id filter.</td><td>integer or null</td><td><code>7</code></td></tr>
</tbody></table>

`CreateLocalBacnetDevice` is the local-object creation request:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this request object.</td><td>string</td><td><code>CREATE_BACNET_DEVICE</code></td></tr>
<tr><td><code>id</code></td><td>Local BACnet device type identifier.</td><td>string</td><td><code>ANALOG_INPUT_TEMPERATURE</code></td></tr>
<tr><td><code>objectId</code></td><td>Optional requested object id. Currently not used.</td><td>integer or null</td><td><code>7</code></td></tr>
<tr><td><code>objectName</code></td><td>Optional requested object name. Currently not used.</td><td>string or null</td><td><code>Room temperature</code></td></tr>
</tbody></table>

`PairBacnetDevice` is the external-object pairing request:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for this request object.</td><td>string</td><td><code>PAIR_BACNET_DEVICE</code></td></tr>
<tr><td><code>deviceId</code></td><td>BACnet device id.</td><td>integer</td><td><code>400001</code></td></tr>
<tr><td><code>objectType</code></td><td>BACnet object type.</td><td>string</td><td><code>analogInput</code></td></tr>
<tr><td><code>objectId</code></td><td>BACnet object id.</td><td>integer</td><td><code>7</code></td></tr>
<tr><td><code>capability</code></td><td>Hub capability that selects the type of Hub device to create.</td><td>string</td><td><code>TEMPERATURE</code></td></tr>
</tbody></table>

Example settings object:

```json
{
  "type": "BACNET_SETTINGS",
  "bacnetEnabled": true,
  "deviceId": 12345,
  "deviceName": "Cozify Hub",
  "port": 47808,
  "broadcast": true
}
```

Example BACnet object identifier:

```json
{
  "type": "BACNET_ID",
  "deviceId": "400001",
  "objectType": "analogInput",
  "objectId": 7,
  "objectName": "Room temperature",
  "address": "192.168.1.50",
  "capability": "TEMPERATURE",
  "locked": false
}
```

## 8. Z-Wave operations

This chapter documents the Z-Wave operations exposed through `POST /hub/protocolconfig`.

Z-Wave commands are sent as a JSON array containing one command object. The command is routed by its `type` field and
the response depends on that command type.

Example request envelope:

```json
[
  {
    "type": "ZWAVE_GET_NODES"
  }
]
```

### 8.1 Available Z-Wave commands

<table>
<thead><tr><th>Command type</th><th>Purpose</th><th>Response</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>ZWAVE_START_INCLUSION</code></td><td>Starts manual Z-Wave inclusion.</td><td><code>ZWAVE_INCLUSION_STATUS</code></td><td>Requires normal Hub pairing to be active.</td></tr>
<tr><td><code>GET_ZWAVE_INCLUSION_STATUS</code></td><td>Returns current inclusion status.</td><td><code>ZWAVE_INCLUSION_STATUS</code></td><td>Requires normal Hub pairing to be active.</td></tr>
<tr><td><code>ZWAVE_CANCEL_INCLUSION</code></td><td>Cancels ongoing inclusion.</td><td><code>ZWAVE_INCLUSION_STATUS</code></td><td>Requires normal Hub pairing to be active.</td></tr>
<tr><td><code>ZWAVE_START_EXCLUSION</code></td><td>Starts Z-Wave exclusion.</td><td><code>ZWAVE_EXCLUSION_STATUS</code></td><td>Does not depend on normal Hub pairing.</td></tr>
<tr><td><code>GET_ZWAVE_EXCLUSION_STATUS</code></td><td>Returns current exclusion status.</td><td><code>ZWAVE_EXCLUSION_STATUS</code></td><td>Does not depend on normal Hub pairing.</td></tr>
<tr><td><code>ZWAVE_CANCEL_EXCLUSION</code></td><td>Cancels ongoing exclusion.</td><td><code>ZWAVE_EXCLUSION_STATUS</code></td><td>Does not depend on normal Hub pairing.</td></tr>
<tr><td><code>ZWAVE_HEAL</code></td><td>Starts network heal.</td><td>boolean</td><td>Returns immediately. The actual heal continues asynchronously, typically for 5 to 30 minutes, and is interrupted if devices are added to or removed from the network.</td></tr>
<tr><td><code>ZWAVE_REMOVE_FAILED</code></td><td>Removes a failed node.</td><td><code>ZWAVE_REMOVE_FAILED_REPLY</code></td><td>Used for nodes already marked failed by the controller.</td></tr>
<tr><td><code>ZWAVE_CHECK_FAILED</code></td><td>Checks whether a node is failed.</td><td><code>ZWAVE_CHECK_FAILED_REPLY</code></td><td>Operates on an existing node id.</td></tr>
<tr><td><code>ZWAVE_REPLACE_FAILED</code></td><td>Starts replacement of a failed node.</td><td>boolean</td><td>Returns immediately after the replace request is sent.</td></tr>
<tr><td><code>ZWAVE_FACTORY_RESET</code></td><td>Factory-resets the Z-Wave module and network.</td><td>boolean</td><td>Clears the Z-Wave network on the Hub.</td></tr>
<tr><td><code>ZWAVE_LEARN</code></td><td>Controls controller learn mode.</td><td>boolean</td><td>Used when adding or removing the controller from another network.</td></tr>
<tr><td><code>ZWAVE_GET_NODES</code></td><td>Returns known Z-Wave nodes.</td><td>array of <code>ZWAVE_NODE</code></td><td>The Hub's own Z-Wave gateway node is not included.</td></tr>
<tr><td><code>ZWAVE_UI_GET_CONFIGURATION</code></td><td>Reads a device configuration parameter.</td><td><code>ZWAVE_UI_CONFIGURATION_REPORT</code></td><td>Requires a supported device driver and Configuration command class support.</td></tr>
<tr><td><code>ZWAVE_UI_SET_CONFIGURATION</code></td><td>Writes a device configuration parameter.</td><td><code>ZWAVE_UI_SET_CONFIGURATION_RESPONSE</code></td><td>Requires a supported device driver and Configuration command class support.</td></tr>
</tbody></table>

### 8.2 Inclusion and exclusion

Z-Wave inclusion is separate from the general Hub scan results, but it is only available while normal device pairing is
active.

The inclusion flow is:

1. Start normal pairing with `GET /hub/scan?ts=0`.
2. Start Z-Wave inclusion with `ZWAVE_START_INCLUSION`.
3. Poll inclusion progress with `GET_ZWAVE_INCLUSION_STATUS`.
4. Optionally cancel inclusion with `ZWAVE_CANCEL_INCLUSION`.
5. Continue normal Hub pairing until the created device appears and can be selected or finalized.

Example request:

```json
[
  {
    "type": "ZWAVE_START_INCLUSION"
  }
]
```

Example response:

```json
{
  "type": "ZWAVE_INCLUSION_STATUS",
  "status": "RUNNING",
  "nodeId": null
}
```

Possible `ZWAVE_INCLUSION_STATUS.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>NOT_PAIRING</code></td><td>Normal Hub pairing is not active, so inclusion can not run.</td></tr>
<tr><td><code>IDLE</code></td><td>Inclusion is available but not currently running.</td></tr>
<tr><td><code>RUNNING</code></td><td>Inclusion is currently running.</td></tr>
<tr><td><code>TIMEOUT</code></td><td>Inclusion timed out.</td></tr>
<tr><td><code>SUCCESS</code></td><td>A node was added successfully. <code>nodeId</code> contains the added node id.</td></tr>
<tr><td><code>CANCEL</code></td><td>Inclusion was canceled.</td></tr>
<tr><td><code>NO_ZWAVE</code></td><td>Z-Wave is not available on this Hub.</td></tr>
<tr><td><code>ERROR</code></td><td>The inclusion request failed.</td></tr>
</tbody></table>

Z-Wave exclusion does not require normal Hub pairing.

Example request:

```json
[
  {
    "type": "ZWAVE_START_EXCLUSION"
  }
]
```

Example response:

```json
{
  "type": "ZWAVE_EXCLUSION_STATUS",
  "status": "RUNNING",
  "nodeId": null
}
```

Possible `ZWAVE_EXCLUSION_STATUS.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>IDLE</code></td><td>Exclusion is not running.</td></tr>
<tr><td><code>RUNNING</code></td><td>Exclusion is currently running.</td></tr>
<tr><td><code>TIMEOUT</code></td><td>Exclusion timed out.</td></tr>
<tr><td><code>SUCCESS</code></td><td>A node was removed successfully. <code>nodeId</code> contains the removed node id.</td></tr>
<tr><td><code>CANCEL</code></td><td>Exclusion was canceled.</td></tr>
<tr><td><code>NO_ZWAVE</code></td><td>Z-Wave is not available on this Hub.</td></tr>
<tr><td><code>ERROR</code></td><td>The exclusion request failed.</td></tr>
</tbody></table>

### 8.3 Network maintenance and node management

`ZWAVE_HEAL` starts a network heal and returns a boolean result immediately.

The heal itself continues asynchronously in the background. In typical installations it may take 5 to 30 minutes to
complete. If devices are added to or removed from the Z-Wave network while the heal is in progress, the heal is
interrupted.

Example request:

```json
[
  {
    "type": "ZWAVE_HEAL"
  }
]
```

Example response:

```json
true
```

`ZWAVE_REMOVE_FAILED` removes a node that the Z-Wave controller already considers failed.

Request attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_REMOVE_FAILED</code></td></tr>
<tr><td><code>nodeId</code></td><td>Node id to remove.</td><td>string</td><td><code>17</code></td></tr>
</tbody></table>

Response attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_REMOVE_FAILED_REPLY</code></td></tr>
<tr><td><code>nodeId</code></td><td>Node id that was processed.</td><td>string</td><td><code>17</code></td></tr>
<tr><td><code>status</code></td><td>Operation status code.</td><td>integer</td><td><code>1</code></td></tr>
<tr><td><code>reason</code></td><td>Human-readable status text.</td><td>string</td><td><code>OK</code></td></tr>
</tbody></table>

Possible `ZWAVE_REMOVE_FAILED_REPLY.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>1</code></td><td>Removal request succeeded.</td></tr>
<tr><td><code>0</code></td><td>The node id was not found in the controller's failed-node list.</td></tr>
<tr><td><code>2</code></td><td>The controller failed to complete the requested process.</td></tr>
<tr><td><code>255</code></td><td>Processing failed on the Hub.</td></tr>
</tbody></table>

Example request:

```json
[
  {
    "type": "ZWAVE_REMOVE_FAILED",
    "nodeId": "17"
  }
]
```

Example response:

```json
{
  "type": "ZWAVE_REMOVE_FAILED_REPLY",
  "nodeId": "17",
  "status": 1,
  "reason": "OK"
}
```

`ZWAVE_CHECK_FAILED` checks whether a node is currently considered failed.

Request and response attributes:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Request type <code>ZWAVE_CHECK_FAILED</code> or response type <code>ZWAVE_CHECK_FAILED_REPLY</code>.</td><td>string</td><td><code>ZWAVE_CHECK_FAILED_REPLY</code></td></tr>
<tr><td><code>nodeId</code></td><td>Node id being checked.</td><td>string</td><td><code>17</code></td></tr>
<tr><td><code>isFailed</code></td><td>Whether the node is failed.</td><td>boolean or null</td><td><code>false</code></td></tr>
<tr><td><code>status</code></td><td>Operation status code.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>reason</code></td><td>Failure reason when status is not successful.</td><td>string or null</td><td><code>null</code></td></tr>
</tbody></table>

Possible `ZWAVE_CHECK_FAILED_REPLY.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>Check succeeded.</td></tr>
<tr><td><code>255</code></td><td>Processing failed on the Hub.</td></tr>
</tbody></table>

Example response:

```json
{
  "type": "ZWAVE_CHECK_FAILED_REPLY",
  "nodeId": "17",
  "isFailed": false,
  "status": 0,
  "reason": null
}
```

`ZWAVE_REPLACE_FAILED` starts replacement of a failed node. It returns `true` if the replace request was accepted and
`false` if it could not be started.

Example request:

```json
[
  {
    "type": "ZWAVE_REPLACE_FAILED",
    "nodeId": "17"
  }
]
```

`ZWAVE_FACTORY_RESET` factory-resets the Z-Wave module and clears the current Z-Wave network on the Hub.

Example request:

```json
[
  {
    "type": "ZWAVE_FACTORY_RESET"
  }
]
```

`ZWAVE_LEARN` controls controller learn mode.

Request attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Allowed values</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_LEARN</code></td></tr>
<tr><td><code>mode</code></td><td>Learn-mode control value.</td><td>integer</td><td><code>0</code>, <code>1</code>, <code>2</code></td></tr>
</tbody></table>

Possible `mode` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>Disable learn mode.</td></tr>
<tr><td><code>1</code></td><td>Classic learn mode.</td></tr>
<tr><td><code>2</code></td><td>Network-wide inclusion learn mode.</td></tr>
</tbody></table>

### 8.4 Listing known Z-Wave nodes

`ZWAVE_GET_NODES` returns a list of known nodes in the Z-Wave network.

Each returned `ZWAVE_NODE` contains:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_NODE</code></td></tr>
<tr><td><code>nodeId</code></td><td>Z-Wave node id.</td><td>string</td><td><code>17</code></td></tr>
<tr><td><code>status</code></td><td>Node status code.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>ageMs</code></td><td>Age of the reported information in milliseconds.</td><td>integer or null</td><td><code>600000</code></td></tr>
<tr><td><code>deviceId</code></td><td>Hub device id created for the node. May be <code>null</code> if the node exists in the Z-Wave network but has no Hub device.</td><td>UUID string or null</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>securityKeys</code></td><td>Granted security-key bit mask.</td><td>integer or null</td><td><code>129</code></td></tr>
<tr><td><code>basicDeviceClass</code></td><td>Basic device class code.</td><td>integer</td><td><code>4</code></td></tr>
<tr><td><code>genericDeviceClass</code></td><td>Generic device class code.</td><td>integer</td><td><code>16</code></td></tr>
<tr><td><code>specificDeviceClass</code></td><td>Specific device class code.</td><td>integer</td><td><code>1</code></td></tr>
<tr><td><code>genericClassName</code></td><td>Text name for the generic class.</td><td>string or null</td><td><code>Binary Switch</code></td></tr>
<tr><td><code>specificClassName</code></td><td>Text name for the specific class.</td><td>string or null</td><td><code>Power Switch Binary</code></td></tr>
<tr><td><code>listening</code></td><td>Whether the node is always listening instead of sleepy.</td><td>boolean</td><td><code>true</code></td></tr>
</tbody></table>

Possible `ZWAVE_NODE.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>The node is known and up-to-date information is available.</td></tr>
<tr><td><code>1</code></td><td>The node is known but fresh information could not be retrieved.</td></tr>
<tr><td><code>2</code></td><td>The node id is unknown.</td></tr>
</tbody></table>

Security key bits in `securityKeys`:

<table>
<thead><tr><th>Bit</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>S2 Unauthenticated</td></tr>
<tr><td><code>1</code></td><td>S2 Authenticated</td></tr>
<tr><td><code>2</code></td><td>S2 Access Control</td></tr>
<tr><td><code>7</code></td><td>Security 0</td></tr>
</tbody></table>

Example request:

```json
[
  {
    "type": "ZWAVE_GET_NODES"
  }
]
```

Example response:

```json
[
  {
    "type": "ZWAVE_NODE",
    "nodeId": "17",
    "status": 0,
    "ageMs": 600000,
    "deviceId": "<device-uuid>",
    "securityKeys": 129,
    "basicDeviceClass": 4,
    "genericDeviceClass": 16,
    "specificDeviceClass": 1,
    "genericClassName": "Binary Switch",
    "specificClassName": "Power Switch Binary",
    "listening": true
  }
]
```

### 8.5 Reading and writing device configuration parameters

The configuration commands are intended for direct access to Z-Wave device configuration parameters.

Support varies by device. A configuration request succeeds only when:

- Z-Wave is available on the Hub
- the node has a matching Hub device driver
- the device supports the Z-Wave Configuration command class

`ZWAVE_UI_GET_CONFIGURATION` reads one parameter value.

Request attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Allowed values</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_UI_GET_CONFIGURATION</code></td></tr>
<tr><td><code>nodeId</code></td><td>Target node id.</td><td>string</td><td>Existing node id</td></tr>
<tr><td><code>parameter</code></td><td>Configuration parameter number.</td><td>integer</td><td><code>1</code> through <code>255</code></td></tr>
</tbody></table>

Response attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_UI_CONFIGURATION_REPORT</code></td></tr>
<tr><td><code>nodeId</code></td><td>Target node id.</td><td>string</td><td><code>17</code></td></tr>
<tr><td><code>parameter</code></td><td>Configuration parameter number.</td><td>integer</td><td><code>14</code></td></tr>
<tr><td><code>size</code></td><td>Parameter size in bytes.</td><td>integer or null</td><td><code>1</code></td></tr>
<tr><td><code>value</code></td><td>Current parameter value.</td><td>integer or null</td><td><code>1</code></td></tr>
<tr><td><code>status</code></td><td>Operation status code.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>reason</code></td><td>Human-readable status text.</td><td>string or null</td><td><code>OK</code></td></tr>
</tbody></table>

Possible `ZWAVE_UI_CONFIGURATION_REPORT.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>Configuration get succeeded.</td></tr>
<tr><td><code>1</code></td><td>Configuration get timed out.</td></tr>
<tr><td><code>2</code></td><td>The device does not support the Configuration command class.</td></tr>
<tr><td><code>255</code></td><td>Processing failed on the Hub.</td></tr>
</tbody></table>

Example request:

```json
[
  {
    "type": "ZWAVE_UI_GET_CONFIGURATION",
    "nodeId": "17",
    "parameter": 14
  }
]
```

Example response:

```json
{
  "type": "ZWAVE_UI_CONFIGURATION_REPORT",
  "nodeId": "17",
  "parameter": 14,
  "size": 1,
  "value": 1,
  "status": 0,
  "reason": "OK"
}
```

`ZWAVE_UI_SET_CONFIGURATION` writes one parameter value.

Request attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Allowed values</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_UI_SET_CONFIGURATION</code></td></tr>
<tr><td><code>nodeId</code></td><td>Target node id.</td><td>string</td><td>Existing node id</td></tr>
<tr><td><code>parameter</code></td><td>Configuration parameter number.</td><td>integer</td><td><code>1</code> through <code>255</code></td></tr>
<tr><td><code>size</code></td><td>Number of bytes used for the parameter value.</td><td>integer</td><td><code>1</code>, <code>2</code>, or <code>4</code></td></tr>
<tr><td><code>value</code></td><td>New parameter value.</td><td>integer or null</td><td><code>1</code></td></tr>
<tr><td><code>default</code></td><td>Whether the parameter should be restored to its default value.</td><td>boolean</td><td><code>false</code></td></tr>
</tbody></table>

If `default=true`, the `value` field is ignored.

Response attributes:

<table>
<thead><tr><th>Attribute</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>ZWAVE_UI_SET_CONFIGURATION_RESPONSE</code></td></tr>
<tr><td><code>nodeId</code></td><td>Target node id.</td><td>string</td><td><code>17</code></td></tr>
<tr><td><code>parameter</code></td><td>Configuration parameter number.</td><td>integer</td><td><code>14</code></td></tr>
<tr><td><code>status</code></td><td>Operation status code.</td><td>integer</td><td><code>0</code></td></tr>
<tr><td><code>reason</code></td><td>Human-readable status text.</td><td>string</td><td><code>OK</code></td></tr>
</tbody></table>

Possible `ZWAVE_UI_SET_CONFIGURATION_RESPONSE.status` values:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td>The Configuration Set was sent to the device.</td></tr>
<tr><td><code>1</code></td><td>The Configuration Set timed out.</td></tr>
<tr><td><code>2</code></td><td>The device does not support the Configuration command class.</td></tr>
<tr><td><code>3</code></td><td>This parameter is already being set. Parallel writes are not allowed.</td></tr>
<tr><td><code>255</code></td><td>Processing failed on the Hub.</td></tr>
</tbody></table>

Status `0` means the set command was sent successfully. It does not by itself guarantee that the physical device
accepted or applied the value.

Example request:

```json
[
  {
    "type": "ZWAVE_UI_SET_CONFIGURATION",
    "nodeId": "17",
    "parameter": 14,
    "size": 1,
    "value": 1,
    "default": false
  }
]
```

Example response:

```json
{
  "type": "ZWAVE_UI_SET_CONFIGURATION_RESPONSE",
  "nodeId": "17",
  "parameter": 14,
  "status": 0,
  "reason": "OK"
}
```

## 9. 433 devices

This chapter documents the manual 433 MHz pairing flow exposed through `GET /hub/433devices` and
`POST /hub/pair433`.

Unlike protocol-command chapters such as Z-Wave or Modbus, 433 pairing does not use `POST /hub/protocolconfig`.
The client first reads the supported 433 pairing classes, then starts pairing for one selected class through the
dedicated endpoint. The actual end-user flow is manual and device-type-specific.

### 9.1 Manual pairing flow

The reference client treats 433 pairing as a guided wizard, not as a one-step API action.

Recommended flow:

1. Start normal Hub pairing with the regular pairing flow when pairing a receiver-style 433 device.
2. Read the available 433 pairing classes with `GET /hub/433devices`.
3. Group the returned classes by `manufacturer` and let the user select the target 433 device type.
4. Show device-specific manual instructions before starting pairing.
5. Start pairing with `POST /hub/pair433?device433=<id>`.
6. Prompt the user to perform the physical device action required for that device type, such as pressing a learn
   button, triggering a sensor, or long-pressing a remote-control key.
7. Observe the created device through the normal device synchronization APIs after the pairing action succeeds.

Important behavior:

- `GET /hub/433devices` returns localized `433_PAIR_INFO` objects based on the request language when translations are
  available
- receiver-style pairings depend on normal Hub pairing being active so that the Hub accepts the discovered 433 message
- `POST /hub/pair433` starts pairing for one selected 433 class; it does not by itself guarantee that a device was
  created
- receiver-style pairings use a short pairing window of 10 seconds in the current Hub implementation
- during that window, the user must trigger the physical device in the way required by the selected device type
- transmitter-style pairings create the Hub-side device immediately and send a protocol-specific pairing signal from
  the Hub to the target device
- the exact wizard copy is device-specific in the reference client, so clients should not hardcode one generic text
  for all 433 devices

The reference client uses separate instruction flows for at least:

- sockets and dimmable outlets
- remote controls and keyfobs
- motion, contact, twilight, and multisensor devices
- smoke alarms and doorbells
- wall transmitters and flush-mounted receivers
- Somfy shutter devices

#### Reference client instructions by 433 device class

The stock client does not use one generic 433 pairing text. It selects one of the following device-specific wizard
flows based on `device433`.

`PROOVE_SWITCH`, `PROOVE_DIM_SWITCH`

1. Connect the outlet to the mains.
2. Keep the outlet programming button pressed for 3 seconds.
3. When the outlet light starts to blink, press the OK button in the client within 10 seconds. This is where the
   client sends `POST /hub/pair433`.
4. When the outlet light stops blinking, check that the outlet appears in the Devices list.

`NEXA_SWITCH`

1. Connect the outlet to the mains.
2. When the outlet light starts to blink, press the OK button in the client within 10 seconds. This is where the
   client sends `POST /hub/pair433`.
3. When the outlet light stops blinking, check that the outlet appears in the Devices list.

`PROOVE_TEMPERATURE`, `PROOVE_MULTISENSOR`

1. If more than one sensor is used, change each sensor to its own transmitting channel before inserting batteries.
2. Turn the sensor on by inserting batteries.
3. Wait 10 minutes.
4. Press the OK button in the client and check that the sensor appears in the Devices list.

The reference client does not show a second countdown screen for this flow.

`PROOVE_RC`

1. Remove the small plastic battery tab from the remote control and bring it near the Hub.
2. Press the OK button in the client when ready. This sends `POST /hub/pair433`.
3. Long-press any key on the remote control within 10 seconds.
4. Press the OK button in the client and verify that the remote control appears in the Devices list.

`NEXA_RC`, `NEXA_KEYFOB`, `NEXA_RC2`, `NEXA_LCD_RC`

1. Bring the remote control near the Hub.
2. Press the OK button in the client when ready. This sends `POST /hub/pair433`.
3. Long-press any key on the remote control within 10 seconds.
4. Press the OK button in the client and verify that the remote control appears in the Devices list.

`PROOVE_MOTION`

1. Prepare the sensor according to the installation guide, for example by installing the battery and bringing it near
   the Hub.
2. Press the OK button in the client when the sensor is ready. This sends `POST /hub/pair433`.
3. Move a hand in front of the sensor within 10 seconds so that the sensor blinks.
4. Press the OK button in the client and verify that the device appears in the Devices list.
5. Install the sensor in its final location.

`NEXA_MOTION`

1. Prepare the sensor according to the installation guide, for example by installing the battery and bringing it near
   the Hub.
2. Press the OK button in the client when the sensor is ready. This sends `POST /hub/pair433`.
3. Set the programming-mode switch to the `SET` position.
4. Press the OK button in the client and verify that the device appears in the Devices list.
5. Install the sensor in its final location.

Unlike the Proove motion flow, the reference client does not show a visible 10-second countdown for `NEXA_MOTION`.

`OPAL_SMOKEALARM`, `AIRAM_SMOKEALARM`, `HM_SMOKEALARM`

1. Prepare the detector according to the installation guide, for example by installing the battery and bringing it
   near the Hub.
2. Press the OK button in the client when the detector is ready. This sends `POST /hub/pair433`.
3. Press the detector's Test button during the next 10 seconds.
4. Press the OK button in the client and check that the detector appears in the Devices list.
5. Install the detector in its final location.

`PROOVE_CONTACT`, `NEXA_CONTACT`

1. Prepare the sensor according to the installation guide, for example by installing the battery and bringing it near
   the Hub.
2. Press the OK button in the client when the sensor is ready. This sends `POST /hub/pair433`.
3. Press the sensor button during the next 10 seconds.
4. Press the OK button in the client and check that the sensor appears in the Devices list.
5. Install the sensor in its final location.

`NEXA_DOORBELL`

1. Remove the small plastic battery tab from the doorbell and bring it near the Hub.
2. Press the OK button in the client when ready. This sends `POST /hub/pair433`.
3. Long-press the doorbell button within 10 seconds.
4. Press the OK button in the client and verify that the doorbell appears in the Devices list.

`NEXA_TWILIGHT`

1. Prepare the sensor according to the installation guide, for example by installing the battery and bringing it near
   the Hub.
2. Press the OK button in the client when the sensor is ready. This sends `POST /hub/pair433`.
3. Press the sensor's START button during the next 10 seconds.
4. Press the OK button in the client and check that the sensor appears in the Devices list.
5. Install the sensor in its final location.

`NEXA_WALLSWITCH1`, `NEXA_WALLSWITCH2`

1. Bring the switch near the Hub.
2. Press the OK button in the client when ready. This sends `POST /hub/pair433`.
3. Press the ON button of the switch within 10 seconds.
4. Press the OK button in the client and verify that the switch appears in the Devices list.

`NEXA_WMR_3500`, `NEXA_LCMR_1000`

1. Ask an electrician to flush-mount the receiver.
2. Press the receiver programming button once.
3. When the receiver light starts to blink, press the OK button in the client within 10 seconds. This sends
   `POST /hub/pair433`.
4. When the receiver light stops blinking, check that the receiver appears in the Devices list.

`NEXA_WMR_1000`

1. Ask an electrician to mount the receiver behind the switch.
2. Press the receiver programming button once.
3. When the receiver light starts to blink, press the OK button in the client within 10 seconds. This sends
   `POST /hub/pair433`.
4. When the receiver light stops blinking, check that the receiver appears in the Devices list.

`NEXA_ALARM`

1. Prepare the doorbell according to the installation guide, for example by installing the battery and bringing it
   near the Hub.
2. Press the doorbell programming button once.
3. When the doorbell light starts to blink, press the OK button in the client within 10 seconds. This sends
   `POST /hub/pair433`.
4. When the doorbell light stops blinking, check that the doorbell appears in the Devices list.

`NEXA_WBT_912`

1. Ask an electrician to flush-mount the transmitter.
2. Press the OK button in the client when mounting is complete. This sends `POST /hub/pair433`.
3. Press the ON button of the switch within 10 seconds.
4. Press the OK button in the client and verify that the transmitter appears in the Devices list.

`SOMFY_SHUTTER`

1. Bring the Somfy device near the Hub.
2. Press the OK button in the client when ready. This sends `POST /hub/pair433`.
3. Press the button of the Somfy device within 10 seconds.
4. Press the OK button in the client and verify that the device appears in the Devices list.

If the client does not recognize the selected `device433`, it falls back to an `Unknown 433 MHz device type`
message and leaves the pairing wizard instead of trying to improvise instructions.

#### Example sequence: manual 433 pairing

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub
    participant Device as 433 Device

    Client->>Hub: GET /cc/<api-version>/hub/scan?ts=0
    Hub-->>Client: SCAN_DELTA {full=true, devices=[...]}
    Note over Hub: Normal Hub pairing is active for receiver-style 433 pairing
    Client->>Hub: GET /cc/<api-version>/hub/433devices
    Hub-->>Client: [433_PAIR_INFO, ...]
    Note over Client: Group classes by manufacturer and device type
    User->>Client: Select 433 device class
    Note over Client: Show device-specific manual instructions
    Client->>Hub: POST /cc/<api-version>/hub/pair433?device433=<selected-id>
    Hub-->>Client: Pairing started
    alt Receiver-style pairing
        Note over Hub: 10 second pairing window starts
        User->>Device: Press learn/test/button or trigger sensor
        Device-->>Hub: 433 discovery message
        Hub-->>Client: Device appears through normal synchronization
    else Transmitter-style pairing
        Hub->>Device: Send pairing signal
        Device-->>Hub: Learns Hub transmitter
        Hub-->>Client: Device appears through normal synchronization
    end
```

### 9.2 433 pairing commands and data

The 433 pairing API uses a small set of explicit data and command objects behind the HTTP endpoints.

```python
class PairingInfo433(Message):
    type = "433_PAIR_INFO"

    def __init__(self, the_id: str=None, name: str=None, manufacturer: str=None):
        super().__init__()
        self.id = the_id
        self.name = name
        self.manufacturer = manufacturer
```

```python
class Get433Classes(UiCmd):
    type = "GET_433_CLSS"
```

```python
class Pair433(UiCmd):
    type = "PAIR_433"

    def __init__(self, device433):
        super().__init__()
        self.device433 = device433
```

`PairingInfo433` is the response object returned by `GET /hub/433devices`:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for a 433 pairing-class description.</td><td>string</td><td><code>433_PAIR_INFO</code></td></tr>
<tr><td><code>id</code></td><td>433 pairing-class identifier used as the <code>device433</code> request parameter in <code>POST /hub/pair433</code>.</td><td>string</td><td><code>NEXA_SWITCH</code></td></tr>
<tr><td><code>name</code></td><td>Localized display name shown to the user.</td><td>string</td><td><code>Nexa power outlet</code></td></tr>
<tr><td><code>manufacturer</code></td><td>Localized or user-facing manufacturer label used by the reference client for grouping the available 433 classes.</td><td>string</td><td><code>Nexa</code></td></tr>
</tbody></table>

`GET_433_CLSS` is the serialized Hub command type behind `GET /hub/433devices`.

`PAIR_433` is the serialized Hub command type behind `POST /hub/pair433`.

Request fields for `PAIR_433`:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for starting 433 pairing.</td><td>string</td><td><code>PAIR_433</code></td></tr>
<tr><td><code>device433</code></td><td>Selected 433 pairing-class id. This must match one of the <code>id</code> values returned by <code>GET /hub/433devices</code>.</td><td>string</td><td><code>NEXA_SWITCH</code></td></tr>
</tbody></table>

Example `GET /hub/433devices` response:

```json
[
  {
    "type": "433_PAIR_INFO",
    "id": "PROOVE_MOTION",
    "name": "Proove motion sensor",
    "manufacturer": "Proove"
  },
  {
    "type": "433_PAIR_INFO",
    "id": "NEXA_SWITCH",
    "name": "Nexa power outlet",
    "manufacturer": "Nexa"
  }
]
```

Equivalent serialized Hub command behind `GET /hub/433devices`:

```json
{
  "type": "GET_433_CLSS"
}
```

Equivalent serialized Hub command behind `POST /hub/pair433?device433=NEXA_SWITCH`:

```json
{
  "type": "PAIR_433",
  "device433": "NEXA_SWITCH"
}
```

Representative 433 class ids currently include:

- `PROOVE_MOTION`
- `PROOVE_CONTACT`
- `PROOVE_SWITCH`
- `PROOVE_DIM_SWITCH`
- `PROOVE_RC`
- `PROOVE_TEMPERATURE`
- `PROOVE_MULTISENSOR`
- `NEXA_SWITCH`
- `NEXA_WMR_3500`
- `NEXA_LCMR_1000`
- `NEXA_WMR_1000`
- `NEXA_ALARM`
- `NEXA_RC`
- `NEXA_RC2`
- `NEXA_LCD_RC`
- `NEXA_DOORBELL`
- `NEXA_KEYFOB`
- `NEXA_TWILIGHT`
- `NEXA_WALLSWITCH1`
- `NEXA_WALLSWITCH2`
- `NEXA_WBT_912`
- `NEXA_MOTION`
- `NEXA_CONTACT`
- `OPAL_SMOKEALARM`
- `AIRAM_SMOKEALARM`
- `HM_SMOKEALARM`
- `SOMFY_SHUTTER` on Hub hardware variants where Somfy 433 support is enabled

### 9.3 Virtual devices created from a device description

Some Hub devices are virtual and do not correspond to a physical device that could be discovered, learned, or
manually triggered during pairing.

For these devices, the client must provide a device description document inside `DC_DEVICE.desc`. The Hub then matches
or creates the device from that description. This is the same autoconfiguration mechanism documented in
[12.1 Autoconfiguration data](#121-autoconfiguration-data), but these virtual-device descriptors are documented here
because they effectively replace physical pairing.

These devices are not 433-specific. They are grouped here because their creation flow is manual in the same practical
sense: the client must construct the pairing document itself.

Important rules:

- these devices are not paired through `GET /hub/433devices`, `POST /hub/pair433`, or normal scan results
- the client must provide a stable synthetic identity in the descriptor so the Hub can match the same logical device
  on later configuration runs
- if no `controller_ref` is supplied, the Hub-side descriptor processing marks the device as `always_create = true`
  when needed
- the full wrapper object is still `DC_DEVICE`; only the contents of `desc` are device-specific

Supported virtual-device descriptors covered here:

<table>
<thead><tr><th>Hub device</th><th>Created device type</th><th>Required <code>desc.type</code></th><th>Required identity field</th></tr></thead>
<tbody>
<tr><td><code>CozifySetpoint</code></td><td><code>GEN_SETPOINT_TEMP_ACTOR</code></td><td><code>GENERIC_SETPOINT_TEMPERATURE_DESC</code></td><td><code>_native_id</code></td></tr>
<tr><td><code>CozifyThermometerMockup</code></td><td><code>VIRTUAL_THERMOMETER</code></td><td><code>VIRTUAL_THERMOMETER_DESC</code></td><td><code>_native_id</code></td></tr>
<tr><td><code>LocalBacnetSignal</code></td><td><code>LOCAL_BACNET_SIGNAL</code></td><td><code>LOCAL_BACNET_HOST</code></td><td><code>id</code></td></tr>
</tbody></table>

#### `CozifySetpoint`

`CozifySetpoint` is a virtual setpoint-temperature device. It does not represent a physical bus or radio device, so
the client must provide a synthetic native id in the descriptor.

Required `desc` fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized descriptor type for a virtual setpoint device.</td><td>string</td><td><code>GENERIC_SETPOINT_TEMPERATURE_DESC</code></td></tr>
<tr><td><code>_native_id</code></td><td>Stable logical identifier used for matching on later runs.</td><td>string</td><td><code>apartment-12-living-room-setpoint</code></td></tr>
</tbody></table>

Minimal required descriptor:

```json
{
  "type": "GENERIC_SETPOINT_TEMPERATURE_DESC",
  "_native_id": "apartment-12-living-room-setpoint"
}
```

Example `DC_DEVICE` wrapper:

```json
{
  "type": "DC_DEVICE",
  "name": "Living room setpoint",
  "locked": true,
  "desc": {
    "type": "GENERIC_SETPOINT_TEMPERATURE_DESC",
    "_native_id": "apartment-12-living-room-setpoint"
  }
}
```

#### `CozifyThermometerMockup`

`CozifyThermometerMockup` is a virtual thermometer device. Like the virtual setpoint device, it is created from a
client-supplied synthetic native id instead of from physical discovery.

Required `desc` fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized descriptor type for a virtual thermometer.</td><td>string</td><td><code>VIRTUAL_THERMOMETER_DESC</code></td></tr>
<tr><td><code>_native_id</code></td><td>Stable logical identifier used for matching on later runs.</td><td>string</td><td><code>apartment-12-living-room-temperature</code></td></tr>
</tbody></table>

Minimal required descriptor:

```json
{
  "type": "VIRTUAL_THERMOMETER_DESC",
  "_native_id": "apartment-12-living-room-temperature"
}
```

Example `DC_DEVICE` wrapper:

```json
{
  "type": "DC_DEVICE",
  "name": "Living room virtual thermometer",
  "locked": true,
  "desc": {
    "type": "VIRTUAL_THERMOMETER_DESC",
    "_native_id": "apartment-12-living-room-temperature"
  }
}
```

#### `LocalBacnetSignal`

`LocalBacnetSignal` is a virtual local BACnet signal object exposed as a Hub device. It is not found through remote
BACnet discovery. Instead, the client defines the local object explicitly with a `LOCAL_BACNET_HOST` descriptor.

Required `desc` fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized descriptor type for a local BACnet object.</td><td>string</td><td><code>LOCAL_BACNET_HOST</code></td></tr>
<tr><td><code>id</code></td><td>Stable logical identifier of the local BACnet object. This is the descriptor's native id.</td><td>string</td><td><code>_29_TEMP_ALARM</code></td></tr>
<tr><td><code>object_type</code></td><td>BACnet object type. For <code>LocalBacnetSignal</code>, this must be <code>binaryValue</code>.</td><td>string</td><td><code>binaryValue</code></td></tr>
<tr><td><code>capability</code></td><td>Selected Hub capability for the object. For <code>LocalBacnetSignal</code>, this must be <code>SIGNAL</code>.</td><td>string</td><td><code>SIGNAL</code></td></tr>
</tbody></table>

Minimal required descriptor:

```json
{
  "type": "LOCAL_BACNET_HOST",
  "id": "_29_TEMP_ALARM",
  "object_type": "binaryValue",
  "capability": "SIGNAL"
}
```

Example `DC_DEVICE` wrapper:

```json
{
  "type": "DC_DEVICE",
  "name": "Floor heating alarm",
  "locked": true,
  "desc": {
    "type": "LOCAL_BACNET_HOST",
    "id": "_29_TEMP_ALARM",
    "object_type": "binaryValue",
    "capability": "SIGNAL"
  }
}
```

Optional additions:

- `room`, `visible`, and `locked` can be set on the outer `DC_DEVICE` wrapper exactly like other autoconfiguration
  devices
- `bacnetIds` may also be included in `LOCAL_BACNET_HOST` when the local BACnet object needs a fixed BACnet object id
  mapping

## 10. Modbus RTU

This chapter documents the manual Modbus RTU pairing operations exposed through `POST /hub/protocolconfig`.

Like the other protocol-command chapters, requests use a JSON array containing one command object. The command is
routed by its `type` field and the response depends on that command type.

Example request envelope:

```json
[
  {
    "type": "GET_MODBUS_PAIRINGS"
  }
]
```

### 10.1 Manual pairing flow

Modbus RTU pairing is not based on automatic scan results. The client drives pairing explicitly through protocol
commands while normal Hub pairing mode is active.

Recommended flow:

1. Start normal Hub pairing with the regular pairing endpoint flow.
2. Send `GET_MODBUS_PAIRINGS` through `POST /hub/protocolconfig` to read the available Modbus pairing templates.
3. Let the user select one template, enter the Modbus slave address, and when required also enter the register and
   register type.
4. Send `PAIR_MODBUS` through `POST /hub/protocolconfig`.
5. If the response is `true`, observe the created device through the normal device state APIs such as `GET /devices`
   or poll/delta synchronization.

Important behavior:

- `GET_MODBUS_PAIRINGS` returns localized template objects based on the request language when translations are
  available
- `PAIR_MODBUS` is accepted only while Modbus pairing is active; otherwise the controller rejects the request
- `address` and `deviceType` are required in `PAIR_MODBUS`
- `register` and `registerType` are required only when the selected pairing template has `registerRequired=true`
- `deviceType` must match one of the templates returned by `GET_MODBUS_PAIRINGS`
- if the target native id already exists on the Hub, pairing is rejected
- for templates with `readDeviceIdSupported=true`, the Hub may perform an additional Modbus device-identification read
  before the final device description is created

#### Example sequence: manual Modbus RTU pairing during normal device pairing

```mermaid
sequenceDiagram
    autonumber
    actor User
    actor Client
    participant Hub

    Client->>Hub: GET /cc/<api-version>/hub/scan?ts=0
    Hub-->>Client: SCAN_DELTA {full=true, devices=[...]}
    Note over Hub: Normal device pairing starts
    Client->>Hub: POST /cc/<api-version>/hub/protocolconfig\n[GET_MODBUS_PAIRINGS]
    Hub-->>Client: [PAIR_MODBUS template, ...]
    par Normal scan polling
        loop While pairing is active
            Client->>Hub: GET /cc/<api-version>/hub/scan?ts=<last-scan-ts>
            Hub-->>Client: SCAN_DELTA {devices=[DEVICE_SCAN, ...]}
        end
    and Manual Modbus RTU pairing UI
        Note over Client: Show returned templates, address input,\nand register input when registerRequired=true
        User->>Client: Select Modbus pairing template and fill address/registers
        Client->>Hub: POST /cc/<api-version>/hub/protocolconfig\n[PAIR_MODBUS {address, register?, registerType?, deviceType, normallyOff}]
        Hub-->>Client: true
        Note over Hub: Hub validates template and creates the Modbus device asynchronously
    end
    Note over Client: Observe the paired device through /devices or normal poll synchronization
```

### 10.2 Protocol configuration commands

#### `GetModbusPairings`

```python
class GetModbusPairings(UiCmd):
    type = "GET_MODBUS_PAIRINGS"
```

Reads the currently supported manual Modbus pairing templates.

Request fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for reading Modbus RTU pairing templates.</td><td>string</td><td><code>GET_MODBUS_PAIRINGS</code></td></tr>
</tbody></table>

Response:

- array of `PairModbus` template objects

Example request:

```json
[
  {
    "type": "GET_MODBUS_PAIRINGS"
  }
]
```

Example response:

```json
[
  {
    "type": "PAIR_MODBUS",
    "deviceType": "RELAY",
    "name": "Relay",
    "manufacturer": "General",
    "registerRequired": true,
    "readDeviceIdSupported": false,
    "normallyOff": true,
    "allowedRegisterTypes": [
      2,
      4
    ]
  },
  {
    "type": "PAIR_MODBUS",
    "deviceType": "PRODUAL_MIO_PAIRER",
    "name": "MIO 12-PT",
    "manufacturer": "Produal",
    "registerRequired": false,
    "readDeviceIdSupported": false,
    "normallyOff": true,
    "allowedRegisterTypes": null
  }
]
```

#### `PairModbus`

```python
class PairModbus(UiCmd):
    type = "PAIR_MODBUS"
```

`PairModbus` is used in two roles:

- as the template object returned by `GET_MODBUS_PAIRINGS`
- as the command object sent back to the Hub when the client performs manual pairing

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Used in request</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td>yes</td><td><code>PAIR_MODBUS</code></td></tr>
<tr><td><code>address</code></td><td>Modbus slave address.</td><td>integer</td><td>yes</td><td><code>12</code></td></tr>
<tr><td><code>register</code></td><td>Register or bit address used by the selected logical device type.</td><td>integer or null</td><td>when <code>registerRequired=true</code></td><td><code>1001</code></td></tr>
<tr><td><code>registerRequired</code></td><td>Whether the client must ask the user for <code>register</code> and <code>registerType</code>.</td><td>boolean</td><td>template metadata</td><td><code>true</code></td></tr>
<tr><td><code>registerType</code></td><td>Modbus register family used by the selected point.</td><td>integer or null</td><td>when <code>registerRequired=true</code></td><td><code>4</code></td></tr>
<tr><td><code>normallyOff</code></td><td>Polarity flag for binary on/off style points. When <code>false</code>, the Hub inverts the raw Modbus on/off value.</td><td>boolean or null</td><td>recommended</td><td><code>true</code></td></tr>
<tr><td><code>deviceType</code></td><td>Hub device type to create. This must match one of the values returned by `GET_MODBUS_PAIRINGS`.</td><td>string</td><td>yes</td><td><code>RELAY</code></td></tr>
<tr><td><code>name</code></td><td>Localized display name for the pairing template.</td><td>string</td><td>template metadata</td><td><code>Relay</code></td></tr>
<tr><td><code>manufacturer</code></td><td>Localized manufacturer label for the pairing template.</td><td>string</td><td>template metadata</td><td><code>General</code></td></tr>
<tr><td><code>readDeviceIdSupported</code></td><td>Whether the Hub may perform additional Modbus device-identification reads before creating the final description.</td><td>boolean</td><td>template metadata</td><td><code>false</code></td></tr>
<tr><td><code>allowedRegisterTypes</code></td><td>Register families accepted for this template when <code>registerRequired=true</code>.</td><td>array of integers or null</td><td>template metadata</td><td><code>[2,4]</code></td></tr>
</tbody></table>

`registerType` values:

<table>
<thead><tr><th>Value</th><th>Name</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>1</code></td><td><code>DISCRETE_INPUT_REGISTER_TYPE</code></td><td>1-bit read-only discrete inputs.</td></tr>
<tr><td><code>2</code></td><td><code>COIL_REGISTER_TYPE</code></td><td>1-bit coils.</td></tr>
<tr><td><code>3</code></td><td><code>INPUT_REGISTER_TYPE</code></td><td>16-bit input registers.</td></tr>
<tr><td><code>4</code></td><td><code>HOLDING_REGISTER_TYPE</code></td><td>16-bit holding registers.</td></tr>
</tbody></table>

Response:

- `true` when the pairing request was accepted for processing

Example request for a general relay:

```json
[
  {
    "type": "PAIR_MODBUS",
    "address": 12,
    "register": 1001,
    "registerType": 4,
    "normallyOff": true,
    "deviceType": "RELAY"
  }
]
```

Example response:

```json
true
```

### 10.3 Modbus device info

`ModbusDeviceInfo` is the typed Modbus metadata object stored with Modbus-backed devices. It carries the effective
addressing and low-level Modbus point information of the paired device.

```python
class ModbusDeviceInfo(object):
    type = "MODBUS_INFO"
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>MODBUS_INFO</code></td></tr>
<tr><td><code>address</code></td><td>Modbus slave address of the paired device.</td><td>integer or null</td><td><code>12</code></td></tr>
<tr><td><code>register</code></td><td>Register or bit address used by the logical point, when applicable.</td><td>integer or null</td><td><code>1001</code></td></tr>
<tr><td><code>registerType</code></td><td>Register family code using the values listed above.</td><td>integer or null</td><td><code>4</code></td></tr>
<tr><td><code>productCode</code></td><td>Product code read from Modbus device-identification data when available.</td><td>string or null</td><td><code>MIO12PT</code></td></tr>
<tr><td><code>revision</code></td><td>Revision string read from Modbus device-identification data when available.</td><td>string or null</td><td><code>1.0</code></td></tr>
<tr><td><code>normallyOff</code></td><td>Effective binary polarity flag used by the Hub for this paired point.</td><td>boolean or null</td><td><code>true</code></td></tr>
</tbody></table>

Example object:

```json
{
  "type": "MODBUS_INFO",
  "address": 12,
  "register": 1001,
  "registerType": 4,
  "productCode": "MIO12PT",
  "revision": "1.0",
  "normallyOff": true
}
```

## 11. Modbus TCP server operations

This chapter documents the Modbus TCP server operations exposed through `POST /hub/protocolconfig`.

Modbus TCP server commands are sent as a JSON array containing one command object. The command is routed by its
`type` field and the response depends on that command type.

Example request envelope:

```json
[
  {
    "type": "GET_MODBUS_TCP_MAPPINGS"
  }
]
```

### 11.1 Availability and network behavior

The Modbus TCP server is available only when the Hub feature `MODBUS_TCP_SERVER` is enabled.

Operational defaults and settings:

- default TCP port is `1234`
- the configured port may be changed with `SET_MODBUS_TCP_CONFIG`
- the configured whitelist is stored with `SET_MODBUS_TCP_CONFIG`
- current Hub versions do not enforce the stored whitelist on incoming TCP connections

If network-level restriction is required, it must be implemented outside the Hub, for example with network
segmentation or firewalling.

### 11.2 Register exposure model

The Modbus TCP server does not automatically publish all devices. Only explicitly mapped capabilities are exposed.

Important exposure rules:

1. A mapping is valid only when the target device exists and has the requested capability.
2. One capability may be mapped once per register type for one device.
3. No two mappings may point to the same `(register_type, register)` address.
4. When a mapped device state changes, the exported Modbus value is updated automatically.
5. When a mapped device is removed from the Hub, all of that device's mappings are removed automatically.

Value conversion rules:

- signed numeric values are exported as signed 16-bit values encoded into the Modbus register space
- scaled values are multiplied before export and divided when written back
- boolean values are exported as `0` or `1`
- `ON_OFF` and `SIGNAL` writes map `0` to off and any nonzero value to on

Reachability handling:

- if no cached state exists yet for a mapped device, the exported value is `null` in the register-list response and
  the register starts without a concrete value
- if the device state is unreachable, integer mappings export `65535` (`0xFFFF`)
- for binary reads, any nonzero internal value is returned as `1`, so an unreachable binary mapping reads as active

### 11.3 Supported Modbus function codes

The server exposes the mapped values through a standard Modbus TCP endpoint.

<table>
<thead><tr><th>Function code</th><th>Name</th><th>Behavior</th></tr></thead>
<tbody>
<tr><td><code>0x01</code></td><td>Read Coils</td><td>Reads mapped coil values.</td></tr>
<tr><td><code>0x02</code></td><td>Read Discrete Inputs</td><td>Reads mapped discrete-input values.</td></tr>
<tr><td><code>0x03</code></td><td>Read Holding Registers</td><td>Reads mapped holding-register values.</td></tr>
<tr><td><code>0x04</code></td><td>Read Input Registers</td><td>Reads mapped input-register values.</td></tr>
<tr><td><code>0x05</code></td><td>Write Single Coil</td><td>Writes one mapped coil when that mapping is writable.</td></tr>
<tr><td><code>0x06</code></td><td>Write Single Register</td><td>Writes one mapped holding register when that mapping is writable.</td></tr>
<tr><td><code>0x0F</code></td><td>Write Multiple Coils</td><td>Writes consecutive mapped coils when the starting mapping is writable.</td></tr>
<tr><td><code>0x10</code></td><td>Write Multiple Registers</td><td>Writes consecutive mapped holding registers when the starting mapping is writable.</td></tr>
<tr><td><code>0x2B</code></td><td>Read Device Identification</td><td>Returns the Hub's basic Modbus device-identification data.</td></tr>
</tbody></table>

Supported device-identification values:

<table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td>VendorName</td><td><code>Cozify</code></td></tr>
<tr><td>ProductCode</td><td><code>HUB</code></td></tr>
<tr><td>MajorMinorRevision</td><td><code>1.0</code></td></tr>
</tbody></table>

Range-handling behavior:

- the starting address of a read or write range must exist
- later gaps inside a read range are returned as `0`
- writes to missing later addresses inside a requested range are ignored
- unsupported function codes return the standard Modbus `ILLEGAL_FUNCTION` exception
- invalid addresses, counts, or write attempts return standard Modbus exception responses

### 11.4 Modbus TCP server data

The Modbus TCP server publishes selected device capabilities as standard Modbus TCP address spaces. Each published
point is defined by one mapping between one Hub device capability and one Modbus register.

Mapping rules:

- the mapped device must exist on the Hub
- the mapped device must have the mapped capability
- the same device capability may be mapped once per register type
- one `(register_type, register)` address may only be used by one mapping
- only the capabilities listed in this section are accepted in mapping requests

#### `CapabilityModbusMapping`

```python
class CapabilityModbusMapping(object):
    type = "CAP_MODBUS_MAPPING"

    def __init__(self, **kwargs):
        super().__init__()
        self.device_id = kwargs.get("device_id", None)
        self.capability = kwargs.get("capability", None)
        self.register_type = kwargs.get("register_type", None)
        self.register = kwargs.get("register", None)
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CAP_MODBUS_MAPPING</code></td></tr>
<tr><td><code>device_id</code></td><td>Hub device id whose capability is exposed through Modbus.</td><td>UUID string</td><td><code>&lt;device-uuid&gt;</code></td></tr>
<tr><td><code>capability</code></td><td>Capability to expose.</td><td>string</td><td><code>TEMPERATURE</code></td></tr>
<tr><td><code>register_type</code></td><td>Modbus address space where the value is published.</td><td>integer</td><td><code>3</code></td></tr>
<tr><td><code>register</code></td><td>Register or bit address inside the selected register type.</td><td>integer</td><td><code>1001</code></td></tr>
</tbody></table>

`register_type` values:

<table>
<thead><tr><th>Value</th><th>Address space</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>1</code></td><td>Discrete inputs</td><td>Read-only binary values.</td></tr>
<tr><td><code>2</code></td><td>Coils</td><td>Binary values, readable and writable when the mapped capability supports writing.</td></tr>
<tr><td><code>3</code></td><td>Input registers</td><td>Read-only 16-bit integer values.</td></tr>
<tr><td><code>4</code></td><td>Holding registers</td><td>16-bit integer values, readable and writable when the mapped capability supports writing.</td></tr>
</tbody></table>

#### Supported mapping capabilities

The following capabilities can currently be published through the Modbus TCP server.

<table>
<thead><tr><th>Capability</th><th>State type</th><th>Attribute identifier</th><th>Explanation</th><th>Attribute type</th><th>Wire conversion</th><th>Modbus write support</th></tr></thead>
<tbody>
<tr><td><code>TEMPERATURE</code></td><td><code>STATE_MULTI_SENSOR</code></td><td><code>temperature</code></td><td>Measured temperature.</td><td>float, degrees Celsius</td><td>Scaled by <code>10</code>, signed 16-bit.</td><td>No</td></tr>
<tr><td><code>SET_TEMPERATURE</code></td><td><code>STATE_MULTI_SENSOR</code></td><td><code>temperature</code></td><td>Settable temperature value published through a multisensor-style state.</td><td>float, degrees Celsius</td><td>Scaled by <code>10</code>, signed 16-bit.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>HUMIDITY</code></td><td><code>STATE_MULTI_SENSOR</code></td><td><code>humidity</code></td><td>Measured relative humidity.</td><td>float, percent</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>ON_OFF</code></td><td><code>STATE_SWITCHABLE</code> family</td><td><code>isOn</code></td><td>Binary on/off state.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>RW_VALUE</code></td><td><code>STATE_RW_VALUE</code></td><td><code>rwValue</code></td><td>Generic read/write numeric value.</td><td>number</td><td>No scaling.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>VALVE</code></td><td><code>STATE_VALVE</code></td><td><code>openPct</code></td><td>Valve opening.</td><td>float, range <code>0.0</code> to <code>1.0</code></td><td>Scaled by <code>100</code>.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>SETPOINT_TEMPERATURE</code></td><td><code>STATE_SETPOINT_TEMPERATURE</code></td><td><code>setpointTemp</code></td><td>Temperature setpoint.</td><td>float, degrees Celsius</td><td>Scaled by <code>10</code>, signed 16-bit.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>FAN_MODE</code></td><td><code>STATE_VENTILATION</code></td><td><code>fanMode</code></td><td>Ventilation fan mode.</td><td>integer enum</td><td>No scaling.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>VENTILATION</code></td><td><code>STATE_VENTILATION</code></td><td><code>mode</code></td><td>Ventilation operating mode.</td><td>integer enum</td><td>No scaling.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>VENTILATION_FN_FIREPLACE</code></td><td><code>STATE_VENTILATION</code></td><td><code>fn_fireplace</code></td><td>Ventilation fireplace function flag.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>No</td></tr>
<tr><td><code>BRIGHTNESS</code></td><td><code>STATE_LIGHT</code></td><td><code>brightness</code></td><td>Light brightness.</td><td>float, range <code>0.0</code> to <code>1.0</code></td><td>Scaled by <code>100</code>.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>LIFT</code></td><td><code>STATE_SHUTTER</code></td><td><code>lift</code></td><td>Shutter lift position.</td><td>number</td><td>No scaling.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>TILT</code></td><td><code>STATE_SHUTTER</code></td><td><code>tilt</code></td><td>Shutter tilt position.</td><td>number</td><td>No scaling.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>BATTERY_U</code></td><td>device state</td><td><code>batteryLow</code></td><td>Battery-low flag.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>No</td></tr>
<tr><td><code>ANALOG_VALUE</code></td><td><code>STATE_ANALOG_VALUE</code> or compatible</td><td><code>value</code></td><td>Generic analog value.</td><td>number</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>ACTIVE_POWER</code></td><td>device state</td><td><code>activePower</code></td><td>Current active power.</td><td>float, watts</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>MEASURE_POWER</code></td><td>device state</td><td><code>totalPower</code></td><td>Total accumulated energy.</td><td>float</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>CONTACT</code></td><td><code>STATE_CONTACT</code></td><td><code>open</code></td><td>Contact open/closed state.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>No</td></tr>
<tr><td><code>MOTION</code></td><td><code>STATE_MOTION</code></td><td><code>motion</code></td><td>Motion detection state.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>No</td></tr>
<tr><td><code>SIGNAL</code></td><td><code>STATE_SIGNAL</code></td><td><code>isOn</code></td><td>Binary signal state.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>Yes, via coil or holding-register mappings.</td></tr>
<tr><td><code>CO2</code></td><td><code>STATE_MULTI_SENSOR</code> or compatible</td><td><code>co2Ppm</code></td><td>Carbon dioxide concentration.</td><td>integer, ppm</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>CO</code></td><td><code>STATE_MULTI_SENSOR</code> or compatible</td><td><code>coDetected</code></td><td>Carbon monoxide alarm state.</td><td>boolean</td><td><code>0</code> or <code>1</code>.</td><td>No</td></tr>
<tr><td><code>PRESSURE</code></td><td><code>STATE_MULTI_SENSOR</code> or compatible</td><td><code>pressure</code></td><td>Measured pressure.</td><td>number</td><td>No scaling.</td><td>No</td></tr>
<tr><td><code>FLOW_VOLUME</code></td><td>device state</td><td><code>volume</code></td><td>Total measured flow volume.</td><td>integer</td><td>No scaling.</td><td>No</td></tr>
</tbody></table>

Possible values for <code>STATE_VENTILATION.mode</code>:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td><code>STOPPED</code></td></tr>
<tr><td><code>1</code></td><td><code>TRAVELING</code></td></tr>
<tr><td><code>2</code></td><td><code>AWAY</code></td></tr>
<tr><td><code>3</code></td><td><code>HOME</code></td></tr>
<tr><td><code>4</code></td><td><code>BOOST</code></td></tr>
</tbody></table>

Possible values for <code>STATE_VENTILATION.fanMode</code>:

<table>
<thead><tr><th>Value</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>0</code></td><td><code>FAN_OFF</code></td></tr>
<tr><td><code>1</code></td><td><code>FAN_MIN</code></td></tr>
<tr><td><code>2</code></td><td><code>FAN_LOW</code></td></tr>
<tr><td><code>3</code></td><td><code>FAN_NORMAL</code></td></tr>
<tr><td><code>4</code></td><td><code>FAN_HIGH</code></td></tr>
<tr><td><code>5</code></td><td><code>FAN_MAX</code></td></tr>
<tr><td><code>6</code></td><td><code>FAN_AUTO</code></td></tr>
</tbody></table>

#### `CapabilityModbusMappings`

```python
class CapabilityModbusMappings(object):
    type = "CAP_MODBUS_MAPPINGS"

    def __init__(self, mappings=None):
        super().__init__()
        self.mappings = mappings
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>CAP_MODBUS_MAPPINGS</code></td></tr>
<tr><td><code>mappings</code></td><td>Full mapping collection.</td><td>array of <code>CAP_MODBUS_MAPPING</code></td><td><code>[{"type":"CAP_MODBUS_MAPPING",...}]</code></td></tr>
</tbody></table>

#### `GetModbusTcpMappings`

```python
class GetModbusTcpMappings(ProtocolConfigCommand):
    type = "GET_MODBUS_TCP_MAPPINGS"
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for reading the current mapping collection.</td><td>string</td><td><code>GET_MODBUS_TCP_MAPPINGS</code></td></tr>
</tbody></table>

#### `SetModbusTcpMappings`

```python
class SetModbusTcpMappings(ProtocolConfigCommand):
    type = "SET_MODBUS_TCP_MAPPINGS"

    def __init__(self):
        super().__init__()
        self.replace = None
        self.mappings = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for creating or updating mappings.</td><td>string</td><td><code>SET_MODBUS_TCP_MAPPINGS</code></td></tr>
<tr><td><code>replace</code></td><td>When <code>true</code>, all existing mappings are cleared before the supplied mappings are applied. When <code>false</code>, only the supplied mapping keys are replaced.</td><td>boolean or null</td><td><code>false</code></td></tr>
<tr><td><code>mappings</code></td><td>Mappings to add or replace.</td><td><code>CAP_MODBUS_MAPPINGS</code> or null</td><td><code>{"type":"CAP_MODBUS_MAPPINGS","mappings":[...]}</code></td></tr>
</tbody></table>

The replace key for one mapping is `(device_id, capability, register_type)`. Changing only the `register` of an
existing mapping moves that capability to the new address inside the same register type.

#### `RemoveModbusTcpMapping`

```python
class RemoveModbusTcpMapping(ProtocolConfigCommand):
    type = "REMOVE_MODBUS_TCP_MAPPING"

    def __init__(self):
        super().__init__()
        self.mapping = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for removing one mapping.</td><td>string</td><td><code>REMOVE_MODBUS_TCP_MAPPING</code></td></tr>
<tr><td><code>mapping</code></td><td>Mapping key to remove.</td><td><code>CAP_MODBUS_MAPPING</code></td><td><code>{"type":"CAP_MODBUS_MAPPING","device_id":"&lt;device-uuid&gt;","capability":"TEMPERATURE","register_type":3,"register":1001}</code></td></tr>
</tbody></table>

The removed mapping is looked up by `(device_id, capability, register_type)`. The `register` field is included for
clarity but is not used as part of the remove key.

#### `GetModbusTcpRegisterList`

```python
class GetModbusTcpRegisterList(ProtocolConfigCommand):
    type = "GET_MODBUS_TCP_REGISTER_LIST"

    def __init__(self):
        super().__init__()
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for reading the current Modbus register list.</td><td>string</td><td><code>GET_MODBUS_TCP_REGISTER_LIST</code></td></tr>
</tbody></table>

Register-list response item fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>device</code></td><td>Current Hub device name for the mapped device.</td><td>string</td><td><code>Living room temperature</code></td></tr>
<tr><td><code>capability</code></td><td>Mapped capability.</td><td>string</td><td><code>TEMPERATURE</code></td></tr>
<tr><td><code>register_type</code></td><td>Published register type.</td><td>integer</td><td><code>3</code></td></tr>
<tr><td><code>register</code></td><td>Published address.</td><td>integer</td><td><code>1001</code></td></tr>
<tr><td><code>scaling</code></td><td>Scaling factor used on the Modbus wire. <code>1</code> means no scaling.</td><td>integer</td><td><code>10</code></td></tr>
<tr><td><code>value</code></td><td>Current Modbus-side register value after scaling and conversion.</td><td>integer or null</td><td><code>215</code></td></tr>
</tbody></table>

#### `SetModbusTcpServerConfig`

```python
class SetModbusTcpServerConfig(ProtocolConfigCommand):
    type = "SET_MODBUS_TCP_CONFIG"

    def __init__(self):
        super().__init__()
        self.port = None
        self.whitelist = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Command discriminator for changing server settings.</td><td>string</td><td><code>SET_MODBUS_TCP_CONFIG</code></td></tr>
<tr><td><code>port</code></td><td>TCP port for the Modbus TCP server.</td><td>integer or null</td><td><code>1502</code></td></tr>
<tr><td><code>whitelist</code></td><td>List of allowed IP addresses to store as the server whitelist.</td><td>array of strings or null</td><td><code>["192.168.1.10","192.168.1.11"]</code></td></tr>
</tbody></table>

Configuration constraints:

- `port` must be an integer and at least `1025`
- `whitelist` must be an array of strings when present
- changing either value restarts the Modbus TCP server when the feature is enabled

#### Example mapping collection

```json
{
  "type": "CAP_MODBUS_MAPPINGS",
  "mappings": [
    {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "TEMPERATURE",
      "register_type": 3,
      "register": 1001
    },
    {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "ON_OFF",
      "register_type": 2,
      "register": 1
    }
  ]
}
```

### 11.5 Protocol configuration commands

Available command types:

<table>
<thead><tr><th>Command type</th><th>Purpose</th><th>Response</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>GET_MODBUS_TCP_MAPPINGS</code></td><td>Returns the current mapping collection.</td><td><code>CAP_MODBUS_MAPPINGS</code></td><td>No request attributes other than <code>type</code>.</td></tr>
<tr><td><code>SET_MODBUS_TCP_MAPPINGS</code></td><td>Creates or updates mappings.</td><td><code>CAP_MODBUS_MAPPINGS</code></td><td>Supports replace or merge behavior.</td></tr>
<tr><td><code>REMOVE_MODBUS_TCP_MAPPING</code></td><td>Removes one mapping.</td><td><code>CAP_MODBUS_MAPPINGS</code></td><td>Fails if the mapping key is not found.</td></tr>
<tr><td><code>GET_MODBUS_TCP_REGISTER_LIST</code></td><td>Returns the current register list with current values.</td><td>array of register-list objects</td><td>Useful for diagnostics and UI previews.</td></tr>
<tr><td><code>SET_MODBUS_TCP_CONFIG</code></td><td>Changes port and whitelist settings.</td><td>boolean</td><td>Restarts the server if settings changed.</td></tr>
</tbody></table>

#### 11.5.1 Reading mappings

Example request:

```json
[
  {
    "type": "GET_MODBUS_TCP_MAPPINGS"
  }
]
```

Example response:

```json
{
  "type": "CAP_MODBUS_MAPPINGS",
  "mappings": [
    {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "TEMPERATURE",
      "register_type": 3,
      "register": 1001
    }
  ]
}
```

#### 11.5.2 Creating or replacing mappings

`SET_MODBUS_TCP_MAPPINGS` supports two modes:

- `replace=true`: clear all old mappings first, then apply the supplied list
- `replace=false`: keep old mappings and replace only mapping keys present in the request

Example request:

```json
[
  {
    "type": "SET_MODBUS_TCP_MAPPINGS",
    "replace": false,
    "mappings": {
      "type": "CAP_MODBUS_MAPPINGS",
      "mappings": [
        {
          "type": "CAP_MODBUS_MAPPING",
          "device_id": "<device-uuid>",
          "capability": "TEMPERATURE",
          "register_type": 3,
          "register": 1001
        },
        {
          "type": "CAP_MODBUS_MAPPING",
          "device_id": "<device-uuid>",
          "capability": "ON_OFF",
          "register_type": 2,
          "register": 1
        }
      ]
    }
  }
]
```

Example response:

```json
{
  "type": "CAP_MODBUS_MAPPINGS",
  "mappings": [
    {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "TEMPERATURE",
      "register_type": 3,
      "register": 1001
    },
    {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "ON_OFF",
      "register_type": 2,
      "register": 1
    }
  ]
}
```

Validation rules:

- every mapping object must be a valid `CAP_MODBUS_MAPPING`
- the capability must be supported by the Modbus TCP server
- the device must exist
- the device must actually have the mapped capability
- no two mappings may target the same `(register_type, register)`

#### 11.5.3 Removing one mapping

Example request:

```json
[
  {
    "type": "REMOVE_MODBUS_TCP_MAPPING",
    "mapping": {
      "type": "CAP_MODBUS_MAPPING",
      "device_id": "<device-uuid>",
      "capability": "TEMPERATURE",
      "register_type": 3,
      "register": 1001
    }
  }
]
```

Example response:

```json
{
  "type": "CAP_MODBUS_MAPPINGS",
  "mappings": []
}
```

If the mapping key is not found, the command fails.

#### 11.5.4 Reading the current register list

The register list is a diagnostic view showing the current published register set, the scaling used on the Modbus
wire, and the current exported value.

Example request:

```json
[
  {
    "type": "GET_MODBUS_TCP_REGISTER_LIST"
  }
]
```

Example response:

```json
[
  {
    "device": "Living room temperature",
    "capability": "TEMPERATURE",
    "register_type": 3,
    "register": 1001,
    "scaling": 10,
    "value": 215
  },
  {
    "device": "Living room light",
    "capability": "ON_OFF",
    "register_type": 2,
    "register": 1,
    "scaling": 1,
    "value": 1
  }
]
```

#### 11.5.5 Changing server settings

Example request:

```json
[
  {
    "type": "SET_MODBUS_TCP_CONFIG",
    "port": 1502,
    "whitelist": [
      "192.168.1.10",
      "192.168.1.11"
    ]
  }
]
```

Example response:

```json
true
```

Changing `port` or `whitelist` stores the new values and restarts the Modbus TCP server when the feature is enabled.

## 12. Autoconfiguration

### 12.1 Autoconfiguration data

`PUT /hub/autoconfig` accepts one `AutoConfiguration` object. This is a high-level installation template used to create
or reuse devices, rooms, scenes, rules, and selected protocol-level configuration in one request.

```python
class AutoConfiguration(AskMessage):
    type = "DC_AUTO_CONFIG"

    def __init__(self):
        super().__init__()
        self.name = None
        self.hubName = None
        self.deviceNamePrefix = None
        self.generateBacnetId = None
        self.modbusTcpBase = None
        self.variables = None
        self.devices = None
        self.removedDevices = None
        self.scenes = None
        self.rules = None
        self.rooms = None
        self.protocols = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator for the autoconfiguration object.</td><td>string</td><td><code>DC_AUTO_CONFIG</code></td></tr>
<tr><td><code>name</code></td><td>Informative name for this configuration run.</td><td>string or null</td><td><code>Reference configuration</code></td></tr>
<tr><td><code>hubName</code></td><td>Optional new Hub name. If omitted, the Hub name is not changed.</td><td>string or null</td><td><code>Example Hub</code></td></tr>
<tr><td><code>deviceNamePrefix</code></td><td>Optional prefix added to every configured device name before processing.</td><td>string or null</td><td><code>Demo </code></td></tr>
<tr><td><code>generateBacnetId</code></td><td>When true, BACnet object ids are generated automatically for Modbus devices that define BACnet ids without explicit object ids.</td><td>boolean or null</td><td><code>true</code></td></tr>
<tr><td><code>modbusTcpBase</code></td><td>Optional register offset added to Modbus TCP mapping registers in protocol commands.</td><td>integer or null</td><td><code>1000</code></td></tr>
<tr><td><code>variables</code></td><td>Variable map used for value substitution. Variables are referenced as <code>${NAME}</code>.</td><td>object or null</td><td><code>{"BCN_DEVICE_ID":1234,"ROOM_NAME":"Living room"}</code></td></tr>
<tr><td><code>devices</code></td><td>Mapping from configuration-local device reference to <code>DCDevice</code>.</td><td>object or null</td><td><code>{"thermometer":{"type":"DC_DEVICE",...}}</code></td></tr>
<tr><td><code>removedDevices</code></td><td>List of existing Hub device ids that should be removed before the new configuration is applied.</td><td>array of strings or null</td><td><code>["&lt;device-uuid&gt;"]</code></td></tr>
<tr><td><code>scenes</code></td><td>Mapping from configuration-local scene reference to <code>DCSceneRef</code>.</td><td>object or null</td><td><code>{"homeScene":{"type":"DC_SCENE_REF",...}}</code></td></tr>
<tr><td><code>rules</code></td><td>List of <code>DCRuleConfig</code> objects.</td><td>array or null</td><td><code>[{"type":"DC_RULE_CONFIG",...}]</code></td></tr>
<tr><td><code>rooms</code></td><td>Optional room configuration object.</td><td><code>DC_ROOM_CONFIG</code> or null</td><td><code>{"type":"DC_ROOM_CONFIG","names":["Living room"],"clear":false}</code></td></tr>
<tr><td><code>protocols</code></td><td>List of protocol configuration commands executed as part of the autoconfiguration.</td><td>array or null</td><td><code>[{"type":"ZWAVE_GET_NODES"}]</code></td></tr>
</tbody></table>

Missing collection fields are normalized to empty values before processing:

- `devices` -> `{}`
- `scenes` -> `{}`
- `rules` -> `[]`
- `variables` -> `{}`
- `protocols` -> `[]`
- `removedDevices` -> `[]`

#### Processing model

Autoconfiguration requests are processed sequentially. One request is fully handled before the next queued request
starts.

The handling flow is:

1. The request is validated and a per-request timeout timer of 60 seconds is started.
2. `deviceNamePrefix` is prepended to each configured device name when present.
3. Variables are resolved in device descriptions, selected room names, and protocol commands when a field value is
   exactly of the form <code>${VARIABLE_NAME}</code>.
4. Existing rooms are resolved and missing rooms are created when needed.
5. `removedDevices` are processed first. Each listed Hub device id is ignored and removed if it exists.
6. Each configured device is matched by native id. Existing devices are reused; missing devices are created.
7. Modbus cache-control protocol commands are executed before new devices are created. Other protocol commands are
   executed after devices, scenes, presets, and rules have been processed.
8. Devices are configured with metadata, visibility, lock state, BACnet ids, and optional per-device configuration
   commands.
9. Scenes are resolved or created, scene presets are updated, and rules are created when they do not already exist
   with the same rule type and rule name.
10. If the configuration succeeds, the request returns `true`. If it fails or times out, the request fails with an
    error and the next queued configuration continues.

Additional handling details:

- BACnet joining is temporarily enabled during device creation when needed and disabled again on success or failure.
- Existing rules are not updated when a rule with the same `configType` and `name` already exists. In that case the
  autoconfiguration leaves that rule unchanged.
- Scene and device references inside the autoconfiguration are internal configuration references. They are resolved to
  actual Hub ids during processing.
- For protocol commands, `SetModbusTcpMappings` receives the `modbusTcpBase` offset by adding that base to each mapped
  register.
- A failed or timed-out autoconfiguration sends a cloud alarm message that includes the configuration name when
  available.

#### Validation and consistency rules

Before changes are applied, the Hub validates the full autoconfiguration payload.

Important validation rules are:

- every top-level field name must be known
- `hubName` and `deviceNamePrefix` must be strings when present
- `generateBacnetId` must be boolean when present
- `modbusTcpBase` must be a non-negative integer when present
- `variables` must be an object when present
- `protocols` must contain protocol configuration commands only
- device references must be unique by physical `native_id`
- an existing device can not appear both in `devices` and `removedDevices`
- every configured device must be supported by the Hub
- rule `configType` must exist among the Hub's available rule templates
- two autoconfigured rules can not use the same rule name

BACnet-specific checks for device descriptions:

- BACnet object ids must be unique across the autoconfiguration and against existing local Hub BACnet points
- BACnet object names must be unique when provided
- the BACnet capability used by one device may appear only once in that device's BACnet point list
- BACnet object types must be supported local object types
- multi-state BACnet objects must define `numberOfStates` and matching `stateText`

If validation fails, nothing from that autoconfiguration request is applied.

#### Variable substitution

Variable substitution is value-based, not template-string-based. A variable is substituted only when the full field
value equals a string of the form <code>${NAME}</code>.

Supported handling in practice:

- device-description fields
- room names referenced through variables
- protocol-command fields

Later in processing, device-reference strings inside rule inputs, rule outputs, scene presets, selected device-command
fields, and configuration-parameter values are replaced with actual Hub device ids.

#### `DCRoomConfig`

```python
class DCRoomConfig(Message):
    type = "DC_ROOM_CONFIG"

    def __init__(self):
        super().__init__()
        self.names = None
        self.clear = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>DC_ROOM_CONFIG</code></td></tr>
<tr><td><code>names</code></td><td>Rooms that should exist after processing.</td><td>array of strings or localized-name objects</td><td><code>["Living room", {"":"Bedroom","fi":"Makuuhuone"}]</code></td></tr>
<tr><td><code>clear</code></td><td>Whether existing rooms not listed in <code>names</code> should be removed first.</td><td>boolean or null</td><td><code>false</code></td></tr>
</tbody></table>

Handling rules:

- room names may be plain strings or localized name objects
- localized room names must define the default language key `""`
- if `clear=true`, existing rooms whose resolved names are not in `names` are removed before missing rooms are created
- device room assignments may also create additional rooms that are not listed in `rooms.names`

#### `DCDevice`

```python
class DCDevice(Message):
    type = "DC_DEVICE"

    def __init__(self):
        super().__init__()
        self.name = None
        self.room = None
        self.locked = None
        self.visible = None
        self.desc = None
        self.config_commands = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>DC_DEVICE</code></td></tr>
<tr><td><code>name</code></td><td>Desired Hub device name.</td><td>string or null</td><td><code>Living room temperature</code></td></tr>
<tr><td><code>room</code></td><td>Room name for the device. May also be a variable reference.</td><td>string or null</td><td><code>${ROOM_NAME}</code></td></tr>
<tr><td><code>locked</code></td><td>Whether the device should be owner-locked for configuration.</td><td>boolean or null</td><td><code>false</code></td></tr>
<tr><td><code>visible</code></td><td>Whether the device should be visible to non-owner users.</td><td>boolean or null</td><td><code>true</code></td></tr>
<tr><td><code>desc</code></td><td>Physical-device description used for pairing or matching.</td><td>protocol-specific <code>BaseDevice</code></td><td><code>{"type":"MODBUS_DEVICE",...}</code></td></tr>
<tr><td><code>config_commands</code></td><td>Extra commands sent to the created or matched device after pairing or matching.</td><td>array of <code>DeviceMetaCommand</code> or <code>Configuration</code></td><td><code>[{"type":"CMD_DEVICE_META",...},{"type":"CONFIGURATION",...}]</code></td></tr>
</tbody></table>

Handling rules:

- `desc` is mandatory and must be a supported `BaseDevice` description
- after variable substitution, the device description may perform additional self-processing before matching
- existing devices are matched by `desc.native_id`
- `name`, `room`, `visible`, and `locked` are applied to both new and existing matched devices when needed
- inside `config_commands`, the `id` field is set by the Hub to the actual matched device id and should be omitted by
  the client
- `DeviceMetaCommand` objects inside `config_commands` are sent only to newly created devices
- `Configuration` objects inside `config_commands` are sent to new devices, and to existing devices only when the
  requested parameter values differ from the current configuration
- when a `DeviceMetaCommand.room` is used inside `config_commands`, it must contain exactly one room name
- string fields and configuration parameter values that equal another autoconfiguration device reference are replaced
  with the actual created or matched Hub device id

#### `DCSceneRef`

```python
class DCSceneRef(Message):
    type = "DC_SCENE_REF"

    def __init__(self):
        super().__init__()
        self.factoryScene = None
        self.name = None
        self.isOn = None
        self.presets = None
        self.requiredIds = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>DC_SCENE_REF</code></td></tr>
<tr><td><code>factoryScene</code></td><td>Factory-scene icon identifier.</td><td>string or null</td><td><code>HOME</code></td></tr>
<tr><td><code>name</code></td><td>Name of a user scene when <code>factoryScene</code> is not used.</td><td>string or null</td><td><code>Evening</code></td></tr>
<tr><td><code>isOn</code></td><td>Initial on/off state when a user scene is created.</td><td>boolean or null</td><td><code>true</code></td></tr>
<tr><td><code>presets</code></td><td>Preset list to store in the scene.</td><td>array of <code>Preset</code> or null</td><td><code>[{"state":{"type":"STATE_LIGHT",...},"targetIds":["lamp"]}]</code></td></tr>
<tr><td><code>requiredIds</code></td><td>Scene references that must be on for this scene.</td><td>array of strings or null</td><td><code>["homeScene"]</code></td></tr>
</tbody></table>

Handling rules:

- when `factoryScene` is set, the scene is resolved from the Hub's factory scenes
- otherwise the scene is resolved by name, and if no matching user scene exists it is created
- `requiredIds` contains configuration-local scene references, not actual scene ids
- `Preset.targetIds` contains configuration-local device references
- when scene presets are updated, existing presets for the same target devices are replaced

#### `DCRuleConfig`

```python
class DCRuleConfig(Message):
    type = "DC_RULE_CONFIG"

    def __init__(self):
        super().__init__()
        self.on_config = None
        self.config = None
```

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>DC_RULE_CONFIG</code></td></tr>
<tr><td><code>on_config</code></td><td>Optional rule-on definition.</td><td><code>RULE_ON_CFG</code> or null</td><td><code>{"type":"RULE_ON_CFG","scenes":["homeScene"],"timers":[]}</code></td></tr>
<tr><td><code>config</code></td><td>Rule configuration.</td><td><code>CONFIG</code></td><td><code>{"type":"CONFIG","configType":"AUTO_LIGHT_RULE",...}</code></td></tr>
</tbody></table>

Handling rules:

- `config` must be a normal `RuleConfig`
- `config.configType` must match an existing Hub rule template
- inside `config.inputs` and `config.outputs`, the values are configuration-local device or scene references, not
  actual Hub ids
- inside `on_config.scenes`, the values are configuration-local scene references
- `config.extras` is copied as provided
- if an extra field is missing, the template default is used through the generated proposal
- if the Hub already has a rule with the same `configType` and `name`, the autoconfiguration does not update it

#### Example autoconfiguration object

```json
{
  "type": "DC_AUTO_CONFIG",
  "name": "Reference configuration",
  "hubName": "Example Hub",
  "deviceNamePrefix": "Demo ",
  "variables": {
    "ROOM_NAME": "Living room"
  },
  "rooms": {
    "type": "DC_ROOM_CONFIG",
    "names": [
      "Living room",
      "Bedroom"
    ],
    "clear": false
  },
  "devices": {
    "thermometer": {
      "type": "DC_DEVICE",
      "name": "Temperature",
      "room": "${ROOM_NAME}",
      "locked": false,
      "visible": true,
      "desc": {
        "type": "MODBUS_DEVICE",
        "address": 2,
        "register": 1,
        "register_type": 3,
        "device_type": "MULTI_SENSOR",
        "vendor_name": "PRODUAL OY",
        "model_name": "MIO 12-PT"
      }
    }
  },
  "scenes": {
    "homeScene": {
      "type": "DC_SCENE_REF",
      "factoryScene": "HOME"
    }
  },
  "protocols": [
    {
      "type": "ZWAVE_GET_NODES"
    }
  ],
  "removedDevices": [
    "<device-uuid>"
  ]
}
```

## Part 4 - Cloud history and cloud video services

## 13. Cloud history service

The history service is a cloud-side API used for historical measurements and event logs.
These calls are separate from the local Hub API served directly by the Hub.

### 13.1 Base URL and authentication

The client uses the cloud API base:

- `https://api.cozify.fi/ui/0.2/` by default
- if a deployment defines a separate history host, the same `/ui/0.2/` path is used on that host

History endpoints are therefore addressed under the same cloud API version path as other cloud calls.

Authentication:

- history requests are cloud requests, not Hub-local requests
- send the cloud user token in the `Authorization` header
- send the token as a raw value, with no `Bearer` prefix
- do not use the HubKey for these history requests

### 13.2 Device history values

Device measurement history is fetched with:

- `PUT /history/device`

Request body fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Request discriminator.</td><td>string</td><td><code>GET_DEVICE_HISTORY_VALUES</code></td></tr>
<tr><td><code>hubId</code></td><td>Target Hub id.</td><td>string</td><td><code>675ec9fe-3af7-11ec-9f96-f8dc7a35d413</code></td></tr>
<tr><td><code>deviceIds</code></td><td>Target device ids.</td><td>array of strings</td><td><code>["&lt;device-uuid&gt;"]</code></td></tr>
<tr><td><code>capabilities</code></td><td>Capability identifiers to fetch.</td><td>array of strings</td><td><code>["TEMPERATURE","HUMIDITY"]</code></td></tr>
<tr><td><code>startTime</code></td><td>Start time in Unix epoch milliseconds.</td><td>integer</td><td><code>1711965600000</code></td></tr>
<tr><td><code>endTime</code></td><td>End time in Unix epoch milliseconds.</td><td>integer</td><td><code>1712052000000</code></td></tr>
<tr><td><code>interval</code></td><td>Requested aggregation interval as a PostgreSQL interval string.</td><td>string</td><td><code>1 hour</code></td></tr>
<tr><td><code>timezone</code></td><td>Requested IANA timezone.</td><td>string</td><td><code>Europe/Helsinki</code></td></tr>
<tr><td><code>responseTimeFormat</code></td><td>Requested timestamp encoding in the response.</td><td>string</td><td><code>ms</code></td></tr>
</tbody></table>

Response shape:

```json
{
  "devices": {
    "4e8b5d32-7b6f-4aa1-8e9a-2f78f01d88d0": {
      "startTime": 1711965600000,
      "endTime": 1712052000000,
      "historyValues": [
        {
          "capability": "TEMPERATURE",
          "values": [
            [1711965600000, 21.5],
            [1711969200000, 21.7]
          ]
        },
        {
          "capability": "HUMIDITY",
          "values": [
            [1711965600000, 41.0],
            [1711969200000, 42.0]
          ]
        }
      ]
    }
  }
}
```

Notes:

- the result is keyed by device id under `devices`
- each device contains `historyValues`
- each `historyValues` item contains one requested capability
- each `values` entry is a two-element array `[timestampMs, value]`

### 13.3 Typical history requests

The stock client uses one history chart per measurement family.
A device with temperature, humidity, and CO2 normally results in separate history requests for each capability, not one combined chart.

Typical capability requests:

<table>
<thead><tr><th>Use case</th><th>Capability list</th><th>Notes</th></tr></thead>
<tbody>
<tr><td>Temperature history</td><td><code>["TEMPERATURE"]</code></td><td>Used for thermostats, multi sensors, contact sensors, moisture sensors, and CO2 sensors that expose temperature.</td></tr>
<tr><td>Humidity history</td><td><code>["HUMIDITY"]</code></td><td>Used for multi sensors and CO2 sensors that expose humidity.</td></tr>
<tr><td>CO2 history</td><td><code>["CO2"]</code></td><td>Used for CO2 sensors.</td></tr>
<tr><td>Power consumption history</td><td><code>["MEASURE_POWER"]</code></td><td>Used for power-consumption charts in kWh.</td></tr>
<tr><td>Water flow / consumption history</td><td><code>["FLOW_VOLUME"]</code></td><td>Used for water-meter history charts in liters.</td></tr>
</tbody></table>

Important distinction for metering devices:

- instantaneous power in watts comes from live state such as `state.activePower`
- power history charts use `MEASURE_POWER`
- instantaneous water flow comes from live state such as `state.flow`
- water history charts use `FLOW_VOLUME`
- daily and total summaries such as `powerToday`, `powerYesterday`, `totalPower`, `volumeToday`, `volumeYesterday`, and `volume` come from live device state, not from the history response payload

Example temperature request:

```json
{
  "type": "GET_DEVICE_HISTORY_VALUES",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "deviceIds": [
    "4e8b5d32-7b6f-4aa1-8e9a-2f78f01d88d0"
  ],
  "capabilities": [
    "TEMPERATURE"
  ],
  "startTime": 1711965600000,
  "endTime": 1712052000000,
  "interval": "1 hour",
  "timezone": "Europe/Helsinki",
  "responseTimeFormat": "ms"
}
```

Example humidity request:

```json
{
  "type": "GET_DEVICE_HISTORY_VALUES",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "deviceIds": [
    "4e8b5d32-7b6f-4aa1-8e9a-2f78f01d88d0"
  ],
  "capabilities": [
    "HUMIDITY"
  ],
  "startTime": 1711965600000,
  "endTime": 1712052000000,
  "interval": "1 hour",
  "timezone": "Europe/Helsinki",
  "responseTimeFormat": "ms"
}
```

Example CO2 request:

```json
{
  "type": "GET_DEVICE_HISTORY_VALUES",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "deviceIds": [
    "7f742ea0-7f80-4d38-8b50-d93c7b3d0111"
  ],
  "capabilities": [
    "CO2"
  ],
  "startTime": 1711965600000,
  "endTime": 1712052000000,
  "interval": "1 hour",
  "timezone": "Europe/Helsinki",
  "responseTimeFormat": "ms"
}
```

Example power-consumption request:

```json
{
  "type": "GET_DEVICE_HISTORY_VALUES",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "deviceIds": [
    "0de0d762-1c30-4f0b-a66c-7e36aa89d6cb"
  ],
  "capabilities": [
    "MEASURE_POWER"
  ],
  "startTime": 1711965600000,
  "endTime": 1712052000000,
  "interval": "1 hour",
  "timezone": "Europe/Helsinki",
  "responseTimeFormat": "ms"
}
```

Example water-meter request:

```json
{
  "type": "GET_DEVICE_HISTORY_VALUES",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "deviceIds": [
    "f642d34e-7740-40e2-a444-18e89d2e4b14"
  ],
  "capabilities": [
    "FLOW_VOLUME"
  ],
  "startTime": 1711965600000,
  "endTime": 1712052000000,
  "interval": "1 hour",
  "timezone": "Europe/Helsinki",
  "responseTimeFormat": "ms"
}
```

### 13.4 Chart period examples

The following history intervals for chart views are commonly used:

<table>
<thead><tr><th>Chart view</th><th>Request period</th><th><code>interval</code> sent to the history service</th></tr></thead>
<tbody>
<tr><td>1 hour</td><td><code>HOUR</code></td><td><code>5 minutes</code></td></tr>
<tr><td>24 hours</td><td><code>DAY</code></td><td><code>1 hour</code></td></tr>
<tr><td>7 days</td><td><code>WEEK</code></td><td><code>6 hours</code></td></tr>
<tr><td>30 days</td><td><code>MONTH</code></td><td><code>1 day</code></td></tr>
<tr><td>1 year</td><td><code>YEAR</code></td><td><code>1 week</code></td></tr>
</tbody></table>

For third-party clients, the most important point is that the service expects a time range in milliseconds together with an aggregation interval.
You do not need to follow the exact same chart policy, but the combinations above are known to work in production.

### 13.5 Hub event history

Hub event history is fetched with:

- `PUT /history/events`

Request body fields:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Request discriminator.</td><td>string</td><td><code>GET_HUB_EVENTS_HISTORY</code></td></tr>
<tr><td><code>hubId</code></td><td>Selected Hub id.</td><td>string</td><td><code>675ec9fe-3af7-11ec-9f96-f8dc7a35d413</code></td></tr>
<tr><td><code>startTime</code></td><td>Start time in Unix epoch milliseconds.</td><td>integer</td><td><code>1712050200000</code></td></tr>
<tr><td><code>endTime</code></td><td>End time in Unix epoch milliseconds.</td><td>integer</td><td><code>1712052000000</code></td></tr>
<tr><td><code>eventTypes</code></td><td>Event type identifiers to include.</td><td>array of strings</td><td><code>["STATE_LIGHT","LIGHT_EVENT","CMD_SCENE_ON"]</code></td></tr>
<tr><td><code>maxCount</code></td><td>Maximum number of events requested.</td><td>integer</td><td><code>1000</code></td></tr>
</tbody></table>

The stock client builds `eventTypes` from three category selections:

- device event types
- rule event types
- scene event types

Event type identifiers used by the client include, for example:

- `COZY_RULE_STATUS`
- `RULE_ON_COMMAND`
- `RULE_OFF_COMMAND`
- `CMD_SCENE_ON`
- `CMD_SCENE_OFF`
- `STATE_LIGHT`
- `LIGHT_EVENT`
- `STATE_CONTACT`
- `CONTACT_EVENT`
- `STATE_MULTI_SENSOR`
- `MOTION_EVENT`

Example request:

```json
{
  "type": "GET_HUB_EVENTS_HISTORY",
  "hubId": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
  "startTime": 1712050200000,
  "endTime": 1712052000000,
  "eventTypes": [
    "STATE_LIGHT",
    "LIGHT_EVENT",
    "CMD_SCENE_ON",
    "COZY_RULE_STATUS"
  ],
  "maxCount": 1000
}
```

Response shape:

```json
{
  "events": {
    "events": [
      {
        "startTime": 1712050200000,
        "endTime": 1712052000000,
        "events": [
          [
            {
              "context": {
                "timestamp": 1712051045123,
                "tz": "Europe/Helsinki",
                "trigger": {
                  "triggerId": "user-123",
                  "role": 8,
                  "remote": false,
                  "ip": "192.168.1.107"
                }
              },
              "event": {
                "id": "4e8b5d32-7b6f-4aa1-8e9a-2f78f01d88d0",
                "type": "STATE_LIGHT",
                "state": {
                  "isOn": true,
                  "brightness": 0.8
                }
              }
            }
          ]
        ]
      }
    ]
  }
}
```

Notes:

- the event list is under `events.events[0].events`
- each list item is an array whose first element contains the actual event wrapper object
- the wrapper object contains `context` and `event`
- `context.timestamp` uses Unix epoch milliseconds
- the event payload may appear in forms such as `event.state`, `event.status.state`, or direct event fields

## 14. Cloud video service

Cloud video features use a separate video service in addition to the normal Hub API.
For third-party clients, the useful pieces are: obtaining a short-lived video token, reading quota, listing recordings, fetching thumbnails and MP4 files, and opening a live WebRTC stream.

### 14.1 Base URLs and authentication

The service uses three base URLs:

- cloud API base for video-token acquisition: `https://api.cozify.fi/ui/0.2/`
- video-on-demand API base for recordings and quota: `https://videoondemand.cozify.fi`
- live streaming base for WebRTC: `wss://videostreaming.cozify.fi`

Authentication rules:

- requests to the cloud API use the normal cloud user token in `Authorization`, without a `Bearer` prefix
- HTTPS requests to the video-on-demand API use the video token in `Authorization`, without a `Bearer` prefix
- playback URLs and live-stream WebSocket URLs carry the video token as query parameter `authorization=<videoToken>`

### 14.2 Obtaining and refreshing a video token

Fetch a video token with:

- `GET https://api.cozify.fi/ui/0.2/video/usertoken`

Request headers:

- `Authorization: <userToken>`

Response shape:

- the response body is the token string itself, not a JSON object

Example response:

```json
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
```

The token is a JWT-like string. A practical client policy is:

- cache the token locally
- decode the payload `exp` if available
- refresh the token before opening video views or video API calls when less than about 10 minutes remain before expiry

### 14.3 Video quota

Fetch recording quota and usage with:

- `GET https://videoondemand.cozify.fi/hubs/{hubId}/quota`

Request headers:

- `Authorization: <videoToken>`

Response fields used by clients include:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>usedClipCount</code></td><td>Current number of stored recordings.</td><td>integer</td></tr>
<tr><td><code>maxClipCount</code></td><td>Maximum allowed number of stored recordings.</td><td>integer</td></tr>
<tr><td><code>usedStoreMB</code></td><td>Used storage in megabytes.</td><td>number</td></tr>
<tr><td><code>maxStoreMB</code></td><td>Maximum storage in megabytes.</td><td>number</td></tr>
<tr><td><code>usedClipCountPercentage</code></td><td>Clip-count usage ratio.</td><td>number</td></tr>
<tr><td><code>usedStoreMBPercentage</code></td><td>Storage-usage ratio.</td><td>number</td></tr>
</tbody></table>

Example response:

```json
{
  "usedClipCount": 152,
  "maxClipCount": 500,
  "usedStoreMB": 2034,
  "maxStoreMB": 10240,
  "usedClipCountPercentage": 0.304,
  "usedStoreMBPercentage": 0.1986328125
}
```

### 14.4 Listing recordings

List recordings for all cameras in a Hub with:

- `GET https://videoondemand.cozify.fi/hubs/{hubId}/recordings?p=1&n=100&o=-created_at`

List recordings for a single camera with:

- `GET https://videoondemand.cozify.fi/hubs/{hubId}/cameras/{cameraId}/recordings?p=1&n=100&o=-created_at`

Important camera identifier rule:

- Camera devices in Hub poll data are proxy devices
- Instead of proxy device object's `id` use the proxy device object's `cameraId` attribute as `{cameraId}` in all video-service URLs
- use the Hub device object's `id` only for normal Hub API commands such as `CMD_VIEW_START` and `CMD_VIEW_STOP`

Example from poll data:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "cameraId": "2cb497c6-1616-4cea-93dd-d9b9f558f110"
}
```

In this case:

- `d8edc980-5d01-4dda-a495-5d1a1eac03e6` is the Hub device id
- `2cb497c6-1616-4cea-93dd-d9b9f558f110` is the video-service camera id

Request headers:

- `Authorization: <videoToken>`

Query parameters used by clients:

<table>
<thead><tr><th>Parameter</th><th>Explanation</th><th>Example</th></tr></thead>
<tbody>
<tr><td><code>p</code></td><td>Page number.</td><td><code>1</code></td></tr>
<tr><td><code>n</code></td><td>Page size.</td><td><code>100</code></td></tr>
<tr><td><code>o</code></td><td>Ordering.</td><td><code>-created_at</code> for newest first</td></tr>
</tbody></table>

Response shape:

```json
{
  "next_page": "/hubs/675ec9fe-3af7-11ec-9f96-f8dc7a35d413/recordings?p=2&n=100&o=-created_at",
  "result": [
    {
      "recording_id": "b8b6c7f6-b9ab-4d7c-bcef-14edb1715af3",
      "camera_id": "8a6a3c5b-ef95-4d11-a3fb-b751fd5f9ab0",
      "hub_id": "675ec9fe-3af7-11ec-9f96-f8dc7a35d413",
      "created_at": 1712051045,
      "video_duration_seconds": 21,
      "video_resolution": "1920x1080",
      "thumbnail": "/hubs/675ec9fe-3af7-11ec-9f96-f8dc7a35d413/cameras/8a6a3c5b-ef95-4d11-a3fb-b751fd5f9ab0/recordings/b8b6c7f6-b9ab-4d7c-bcef-14edb1715af3/thumbnail",
      "video": "/hubs/675ec9fe-3af7-11ec-9f96-f8dc7a35d413/cameras/8a6a3c5b-ef95-4d11-a3fb-b751fd5f9ab0/recordings/b8b6c7f6-b9ab-4d7c-bcef-14edb1715af3/video",
      "file_size_bytes": 1894400,
      "name": "Front door"
    }
  ]
}
```

Notes:

- `created_at` is in Unix epoch seconds
- `next_page` is a relative path; when present, prefix it with `https://videoondemand.cozify.fi`
- the fields under `result[]` are enough to build a recording list without extra Hub API calls

### 14.5 Playing, renaming, and deleting recordings

For thumbnails and MP4 playback, prefix the relative `thumbnail` and `video` paths from the list response with the video-on-demand base URL and append the video token as query parameter.

Example thumbnail URL:

```text
https://videoondemand.cozify.fi/hubs/{hubId}/cameras/{cameraId}/recordings/{recordingId}/thumbnail?authorization={videoToken}
```

Example MP4 URL:

```text
https://videoondemand.cozify.fi/hubs/{hubId}/cameras/{cameraId}/recordings/{recordingId}/video?authorization={videoToken}
```

Rename a recording with:

- `PUT https://videoondemand.cozify.fi/hubs/{hubId}/cameras/{cameraId}/recordings/{recordingId}`

Request headers:

- `Authorization: <videoToken>`

Request body:

```json
{
  "db": {
    "name": "Front door package delivery"
  }
}
```

Delete a recording with:

- `DELETE https://videoondemand.cozify.fi/hubs/{hubId}/cameras/{cameraId}/recordings/{recordingId}`

Request headers:

- `Authorization: <videoToken>`

The stock client deletes multiple recordings by repeating the single-recording `DELETE` call. No batch-delete endpoint was observed.

### 14.6 Live view and recording control

Live video uses WebRTC over a WebSocket signaling channel, not MP4 playback.

The Hub camera device and the video service have different roles:

- Hub device commands control the camera stream lifecycle and recording state
- the video service carries the live WebRTC session and the stored recordings
- for proxy camera devices, Hub commands use the proxy device `id` and video-service URLs use `device.cameraId`

Command behavior summary:

<table>
<thead><tr><th>Command</th><th>Main effect</th><th>What happens if camera is currently off</th><th>What happens if recording is already active</th></tr></thead>
<tbody>
<tr><td><code>DeviceCommand</code></td><td>Applies the supplied <code>state.isOn</code>, <code>state.recording</code>, and optionally <code>state.media</code>.</td><td>Depends on the state payload. <code>recording=true</code> starts the stream and then starts recording. <code>isOn=true</code> starts the stream without recording.</td><td>Depends on the state payload. For example, <code>recording=false</code> stops recording but keeps the camera on, while <code>isOn=false</code> turns the camera fully off.</td></tr>
<tr><td><code>DeviceOnCommand</code></td><td>Turns the camera stream on.</td><td>Starts the stream.</td><td>If the camera is already recording, recording stays on.</td></tr>
<tr><td><code>DeviceOffCommand</code></td><td>Turns the camera fully off.</td><td>No practical effect if it is already off.</td><td>Stops recording and then stops the stream.</td></tr>
<tr><td><code>ViewStartCommand</code></td><td>Marks a local viewer and turns the stream on.</td><td>Starts the stream.</td><td>Keeps recording active. It does not start a new recording.</td></tr>
<tr><td><code>ViewStopCommand</code></td><td>Removes the local viewer marker.</td><td>No practical effect if the camera is already off.</td><td>If recording is active, the stream stays on. If recording is not active and no local viewers remain, the stream is turned off.</td></tr>
<tr><td><code>RecordOnCommand</code></td><td>Starts recording.</td><td>Starts the stream and then starts recording.</td><td>Keeps the camera in recording mode and updates clip-length / recorder tracking.</td></tr>
<tr><td><code>RecordOffCommand</code></td><td>Stops recording.</td><td>No practical effect if it is already off and not recording.</td><td>Stops recording but leaves the camera stream on. Later idle-view logic may turn the stream off if nobody is viewing.</td></tr>
</tbody></table>

Important consequence:

- `CMD_RECORD_ON` is enough to bring an off camera up and start recording
- because recording requires an active stream, the camera stream is started first
- after that, live view can connect to the normal WebRTC URL
- `CMD_RECORD_ON` does not replace `CMD_VIEW_START` as the viewer-tracking command used by clients for live viewing

For `DeviceCommand`, the effective behavior comes from the state payload:

- `{"state":{"isOn":true}}` means stream on, preserving the current recording target
- `{"state":{"isOn":false}}` means stream off and recording off
- `{"state":{"recording":true}}` means recording mode; if needed, start the stream first and then start recording
- `{"state":{"recording":false}}` means stop recording, but otherwise keep the current on/off target
- `{"state":{"isOn":true,"recording":true}}` means stream on and recording on
- `{"state":{"isOn":true,"recording":false}}` means stream on and recording off
- changing `state.media` while the stream is active causes the camera stream to restart with the new media profile

Observed live-stream URL shape:

```text
wss://videostreaming.cozify.fi/hubs/{hubId}/cameras/{cameraId}/webrtc?authorization={videoToken}
```

A third-party client opening live view should follow this sequence:

1. Read the camera proxy device from Hub poll data and take its `cameraId` field.
2. Send `CMD_VIEW_START` to the camera device through the normal Hub API using the Hub device `id`.
3. Open the WebSocket URL above using `cameraId` from the poll data, not the Hub device `id`.
4. Perform the WebRTC offer/answer exchange.
5. When live view is closed, send `CMD_VIEW_STOP` to the camera device using the Hub device `id`.

The identifier split is important:

- Hub commands use the Hub device `id`
- video-service URLs use `device.cameraId`
- the values may be different because the Hub camera device can be a proxy object

Example live-stream URL using the poll example above:

```text
wss://videostreaming.cozify.fi/hubs/{hubId}/cameras/2cb497c6-1616-4cea-93dd-d9b9f558f110/webrtc?authorization={videoToken}
```

The corresponding Hub commands still target the proxy device id:

1. `CMD_VIEW_START` with `id: "d8edc980-5d01-4dda-a495-5d1a1eac03e6"`
2. `CMD_VIEW_STOP` with `id: "d8edc980-5d01-4dda-a495-5d1a1eac03e6"`

Example Hub command to mark the start of a live view:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_VIEW_START",
  "uiId": "web-myclient-01"
}
```

Example Hub command to mark the end of a live view:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_VIEW_STOP",
  "uiId": "web-myclient-01"
}
```

Recording control uses normal Hub API device commands, not the video-service HTTP API.
Use the same proxy device `id` that you use for `CMD_VIEW_START` and `CMD_VIEW_STOP`.

The reference client toggles recording with these commands:

- `CMD_RECORD_ON`
- `CMD_RECORD_OFF`

These commands correspond to camera state `state.recording` and are relevant for devices with video-recording capability such as `RECORD_VIDEO`.

Example Hub command to start recording:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_RECORD_ON"
}
```

`CMD_RECORD_ON` may also include clip length information. The driver stores `clipLengthMs` and stops the recording automatically when that timer expires.

Example timed recording command:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_RECORD_ON",
  "clipLengthMs": 30000
}
```

Example Hub command to stop recording:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_RECORD_OFF"
}
```

`CMD_RECORD_OFF` may request cancellation of the current clip:

```json
{
  "id": "d8edc980-5d01-4dda-a495-5d1a1eac03e6",
  "type": "CMD_RECORD_OFF",
  "cancelRecording": true
}
```

Identifier rule for recording is the same as for live view:

- send `CMD_RECORD_ON` and `CMD_RECORD_OFF` to the Hub proxy device `id`
- use `device.cameraId` only in video-service URLs

For `uiId`, use a stable identifier for the client application instance.
`ViewStartCommand` and `ViewStopCommand` use `uiId`; the recording commands do not.

#### Foscam-specific setup and onboarding commands

These commands are relevant for cameras that expose capability `FOSCAM_API`. The stock client treats that capability as the switch for Foscam-specific setup UI such as reset instructions and Wi-Fi configuration. Cameras with capability `ONVIF_API` go through the ONVIF-oriented flow instead, and cameras with neither capability fall back to a more generic credential prompt flow.

Scope note:

- all cameras: live-view commands, record on/off commands, `CMD_DEVICE` with `STATE_CAMERA`, and `REFRESH_DEVICE`
- credential-managed cameras in general: `USE_CREDENTIALS_CMD`
- Foscam only: `CHANGE_CREDENTIALS_CMD`, `SCAN_WIFIS_CMD`, `SET_WIFI_CMD`, `FOSCAM_AT_IP`, and the Wi-Fi/reset-instructions UI branches

The Foscam-specific setup commands are sent through `PUT /devices/command` and return immediate results instead of the normal asynchronous dispatch behavior:

<table>
<thead><tr><th>Command type</th><th>Scope</th><th>Source command</th><th>Payload fields</th><th>Immediate result</th><th>Meaning</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>USE_CREDENTIALS_CMD</code></td><td>Credential-managed cameras in general</td><td><code>UseCredentials</code></td><td><code>id</code>, <code>username</code>, <code>password</code></td><td>boolean</td><td>Tests the supplied credentials against the camera and, on success, stores them for subsequent Hub-to-camera communication.</td><td><pre><code class="language-json">{
  "type": "USE_CREDENTIALS_CMD",
  "id": "&lt;camera-device-uuid&gt;",
  "username": "admin",
  "password": "secret"
}</code></pre></td></tr>
<tr><td><code>CHANGE_CREDENTIALS_CMD</code></td><td>Foscam only</td><td><code>ChangeCredentials</code></td><td><code>id</code>, <code>password</code>, optional <code>username</code></td><td>boolean</td><td>Changes the Foscam admin credentials on the camera and updates the Hub-side stored credentials. In current implementation, username change is handled in the same command.</td><td><pre><code class="language-json">{
  "type": "CHANGE_CREDENTIALS_CMD",
  "id": "&lt;camera-device-uuid&gt;",
  "username": "admin",
  "password": "new-secret"
}</code></pre></td></tr>
<tr><td><code>SCAN_WIFIS_CMD</code></td><td>Foscam only</td><td><code>ScanWifis</code></td><td><code>id</code></td><td>array of <code>WIFI_ACCESS_POINT</code></td><td>Refreshes and returns the list of Wi-Fi access points currently visible to the Foscam camera.</td><td><pre><code class="language-json">{
  "type": "SCAN_WIFIS_CMD",
  "id": "&lt;camera-device-uuid&gt;"
}</code></pre></td></tr>
<tr><td><code>SET_WIFI_CMD</code></td><td>Foscam only</td><td><code>SetWifi</code></td><td><code>id</code>, <code>ap</code>, <code>psk</code></td><td>boolean</td><td>Configures the camera to use the selected Wi-Fi access point and pre-shared key.</td><td><pre><code class="language-json">{
  "type": "SET_WIFI_CMD",
  "id": "&lt;camera-device-uuid&gt;",
  "ap": {
    "type": "WIFI_ACCESS_POINT",
    "ssid": "Office WiFi",
    "ssidKey": "Office+WiFi",
    "encrypted": true,
    "encryptionMode": "WPA2"
  },
  "psk": "correct horse battery staple"
}</code></pre></td></tr>
</tbody></table>

`SCAN_WIFIS_CMD` returns `WIFI_ACCESS_POINT` objects shaped like:

<table>
<thead><tr><th>Field</th><th>Explanation</th><th>Type</th><th>Example value</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Serialized type discriminator.</td><td>string</td><td><code>WIFI_ACCESS_POINT</code></td></tr>
<tr><td><code>ssid</code></td><td>Human-readable Wi-Fi network name.</td><td>string</td><td><code>Office WiFi</code></td></tr>
<tr><td><code>ssidKey</code></td><td>SSID key used in the camera API when applying Wi-Fi settings.</td><td>string</td><td><code>Office+WiFi</code></td></tr>
<tr><td><code>mac</code></td><td>Access-point MAC address if reported by the camera.</td><td>string or null</td><td><code>aa:bb:cc:dd:ee:ff</code></td></tr>
<tr><td><code>quality</code></td><td>Signal-quality indicator reported by the camera.</td><td>number or string or null</td><td><code>72</code></td></tr>
<tr><td><code>encrypted</code></td><td>Whether the access point requires encryption.</td><td>boolean</td><td><code>true</code></td></tr>
<tr><td><code>encryptionMode</code></td><td>Encryption mode reported by the camera.</td><td>string</td><td><code>WPA2</code></td></tr>
</tbody></table>

Observed implementation details:

- generic credential flow: `USE_CREDENTIALS_CMD` returns `true` only when the supplied credentials log in successfully with admin privileges
- Foscam only: `CHANGE_CREDENTIALS_CMD` returns `false` if the new username/password is rejected before or during the camera API call
- Foscam only: `SET_WIFI_CMD` may test the provided Wi-Fi parameters before applying them when the camera is still connected by cable
- Foscam only: these setup commands are handled by the Foscam driver; the stock client only exposes them for cameras identified as Foscam-capable

WebRTC signaling messages observed on the WebSocket:

- client to server: `{"id":"sdpOffer","sdpOffer":"..."}`
- client to server: `{"id":"iceCandidate","candidate":{...}}`
- client to server: `{"id":"iceCandidate","candidate":null}` when local ICE gathering completes
- server to client: `{"id":"ready"}`
- server to client: `{"id":"sdpAnswer","sdpAnswer":"..."}`
- server to client: `{"id":"iceCandidate","candidate":{...}}`
- server to client on failure: `{"error":"MaxLiveViewCountExceeded"}`

The observed ICE server list is:

- `stun:stun.cozify.fi`
- `stun:stun.l.google.com:19302`

## Part 5 - Custom rules

## 15. Custom rules

This chapter documents the Hub functionality for installing, reading, logging, and removing custom rules.

Custom rules are not a separate runtime model from ordinary rules. Once installed, a custom rule appears in the normal
rule-template listing and is then created, updated, enabled, disabled, and deleted through the same rule endpoints as
factory rules.

### 15.1 Availability and lifecycle

Custom rule management is controlled by the Hub feature `CUSTOM_RULES`.

Feature-gated operations:

- `POST /rules/template` requires `CUSTOM_RULES`
- `DELETE /rules/template` requires `CUSTOM_RULES`
- `GET /rules/log` requires `CUSTOM_RULES`

`GET /rules/template` is not feature-gated in the same way, but it only returns stored custom-rule source when the
requested rule type exists in the persisted custom-rule set.

Lifecycle:

1. Install custom rule source with `POST /rules/template`.
2. The Hub parses the source, creates a custom rule class, and adds it to the normal rule-template set.
3. The installed rule appears in `GET /rules/templates` with category `CUSTOM_RULE`.
4. Create or update rule instances of that type with normal `PUT /rules` requests.
5. Control those instances with the normal rule endpoints such as `PUT /rules/command` and `PUT /rules/onConfiguration`.
6. Read the stored source with `GET /rules/template` or runtime log output with `GET /rules/log`.
7. Remove the custom rule type with `DELETE /rules/template`.

Persistence behavior:

- installed custom-rule source is persisted by rule type
- on Hub startup, persisted custom-rule sources are parsed and reinstalled automatically
- uninstalling a custom rule also removes existing rule instances of that rule type

### 15.2 Device status and event messages

This section documents the runtime message wrappers that are delivered to custom-rule callbacks.

#### Device status message

`DeviceStatusMessage` is the wrapper used when the rule runtime delivers a status snapshot to `on_status(status)`.

Serialized type:

- `DEV_STATUS_MSG`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Actor id of the device whose status was reported.</td><td>string</td></tr>
<tr><td><code>status</code></td><td>The current device status object. This status object also contains a <code>state</code> attribute with the device's current state snapshot.</td><td>typed status object</td></tr>
</tbody></table>

Callback behavior:

- `on_status()` receives only the `status` field, not the full wrapper message
- the concrete status object type depends on the device and may be, for example, `LightStatus`, `MotionSensorStatus`, `PowerMeterStatus`, or another device-specific status type
- rules can read the current state snapshot from `status.state`
- when the Hub processes a `DeviceStatusMessage`, it also emits a matching `DeviceStateMessage` built from `status.id` and `status.status.state`
- because of that, a device status update may trigger both `on_status(status)` and `on_event(ev)` for the same source device

Example runtime message:

```json
{
  "type": "DEV_STATUS_MSG",
  "id": "device-id",
  "status": {
    "type": "STATUS_LIGHT",
    "id": "device-id",
    "name": "Kitchen light",
    "reachable": true,
    "state": {
      "type": "STATE_LIGHT",
      "isOn": true,
      "brightness": 100
    }
  }
}
```

#### Device state message

`DeviceStateMessage` is the wrapper used when the rule runtime delivers a state-change event to `on_event(ev)`.

Serialized type:

- `DEV_STATE_EVENT`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Actor id of the device that produced the event.</td><td>string</td></tr>
<tr><td><code>state</code></td><td>The new device state object.</td><td>typed state object</td></tr>
</tbody></table>

Callback behavior:

- `on_event()` receives the full event object
- rules typically use `ev.id` to identify the source and `ev.state` to read the changed values
- concrete event classes such as `MOTION_EVENT`, `LIGHT_EVENT`, `CONTACT_EVENT`, and `VALUE_EVENT` are subclasses of `DeviceStateMessage`

Example runtime message:

```json
{
  "type": "MOTION_EVENT",
  "id": "sensor-id",
  "state": {
    "type": "STATE_MOTION",
    "motion": true
  }
}
```

#### Alert

`Alert` is the object delivered to `on_alert(ev)`.

Serialized type:

- `ALERT`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Alert id.</td><td>string</td></tr>
<tr><td><code>sourceId</code></td><td>Source actor id associated with the alert.</td><td>string</td></tr>
<tr><td><code>message</code></td><td>Alert message template or localized message object.</td><td>string or localized object</td></tr>
<tr><td><code>error</code></td><td><code>true</code> for an error alert, <code>false</code> for a warning-style alert.</td><td>boolean</td></tr>
<tr><td><code>cleared</code></td><td>Whether the alert has been cleared.</td><td>boolean</td></tr>
<tr><td><code>realtimeMs</code></td><td>Creation time in milliseconds.</td><td>integer</td></tr>
<tr><td><code>timestamp</code></td><td>Internal attribute.</td><td>integer or null</td></tr>
<tr><td><code>userId</code></td><td>Internal attribute.</td><td>string or null</td></tr>
</tbody></table>

Example runtime message:

```json
{
  "type": "ALERT",
  "id": "alert-id",
  "sourceId": "device-id",
  "message": "Battery low",
  "error": false,
  "cleared": false,
  "realtimeMs": 1774441252087,
  "timestamp": null,
  "userId": null
}
```

#### Generic alarm event from a rule

`GenericAlarmEvent` is the object delivered to `on_generic_alert_from_rule(ev)`.

Serialized type:

- `G_ALARM_EVENT`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Id (typically a device) associated with the generic alarm event.</td><td>string</td></tr>
<tr><td><code>state</code></td><td>Alarm state payload created with <code>create_general_alarm_in_state()</code>.</td><td><code>DeviceState</code>-shaped object</td></tr>
</tbody></table>

Behavior:

- `GenericAlarmEvent` is a `DeviceStateMessage` subclass
- while the event is usually created by a device, a rule may send it on behalf of a device with `send_command(...)` allowing custom alarms
- that state contains:
- `gAlarm`: boolean active/acknowledged flag
- `gAlarmAt`: alarm activation timestamp when active, or alarm clear timestamp when closed
- `gAlarmId`: alarm id
- `gAlarmCode`: alarm code
- `gAlarmMsg`: translated alarm message
- `reachable`: copied from the underlying device state
- `lastSeen`: copied from the underlying device state
- capability metadata may exist internally in the runtime, but it is not part of the serialized API shape

Example runtime message:

```json
{
  "type": "G_ALARM_EVENT",
  "id": "actor-id",
  "state": {
    "gAlarm": true,
    "gAlarmAt": 1774441252087,
    "gAlarmId": "alarm-id",
    "gAlarmCode": "DEVICE_FAULT",
    "gAlarmMsg": "Device fault",
    "reachable": true,
    "lastSeen": 1774441252000
  }
}
```

#### User session event

`UserSessionEvent` may also be delivered to `on_event(ev)` for rules that listen to user-session inputs.

Serialized type:

- `USER_SESSION`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>user_id</code></td><td>User id associated with the session change.</td><td>string</td></tr>
<tr><td><code>ip</code></td><td>Client IP address.</td><td>string or null</td></tr>
<tr><td><code>remote_call</code></td><td>Whether the session is remote.</td><td>boolean or null</td></tr>
<tr><td><code>session_started</code></td><td><code>true</code> when the session started, <code>false</code> when it ended.</td><td>boolean or null</td></tr>
<tr><td><code>ui_uuid</code></td><td>UI/client instance id.</td><td>string or null</td></tr>
</tbody></table>

Example runtime message:

```json
{
  "type": "USER_SESSION",
  "user_id": "user-id",
  "ip": "192.168.1.50",
  "remote_call": false,
  "session_started": true,
  "ui_uuid": "client-instance-id"
}
```

### 15.3 Rule base class and template types

Custom rule source defines one Python class derived from `Rule`.

The `Rule` base class defines:

- the static metadata every rule exposes
- convenience constants for selector types and extra-value types
- the descriptor types used to declare inputs, outputs, and extras
- the runtime object model that the Hub turns into `RuleTemplate` and `RuleConfig` data

#### Static rule metadata

A rule class normally defines these class attributes:

<table>
<thead><tr><th>Attribute</th><th>Purpose</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>Unique rule/template type identifier.</td><td>Becomes <code>RuleTemplate.configType</code> and later <code>RuleConfig.configType</code>.</td></tr>
<tr><td><code>name</code></td><td>User-facing rule name.</td><td>May be a plain string or a localized dictionary.</td></tr>
<tr><td><code>description</code></td><td>User-facing longer description.</td><td>May be a plain string or a localized dictionary.</td></tr>
<tr><td><code>summary</code></td><td>Short workflow summary shown in the UI.</td><td>May be a plain string or a localized dictionary.</td></tr>
<tr><td><code>category</code></td><td>Rule category.</td><td>For custom rules the Hub forces this to <code>CUSTOM_RULE</code> after parsing.</td></tr>
<tr><td><code>logger</code></td><td>Rule logger.</td><td><code>logging.Logger</code>. Available on the base class and rebound for custom rules at runtime.</td></tr>
</tbody></table>

At runtime the base-class constructor also provides instance attributes `name`, `description`, `summary`, and `category`.

Example rule class shape:

```python
class ExampleRule(Rule):
    type = "EXAMPLE_RULE"
    name = "Example rule"
    description = "Example rule description"
    summary = "Example rule summary"

    sensors = Input(
        field_type=Rule.MOTION_SENSOR,  # Can be None if MOTION capability is only requirement
        capabilities=[Capability.MOTION],
        display_order=1,
        description="Motion sensor",
        min_count=1
    )

    devices = Output(
        field_type=None,  # All devices with CONTROL_LIGHT capability accepted
        capabilities=[Capability.CONTROL_LIGHT],
        display_order=2,
        description="Controlled light",
        min_count=1
    )

    timeout = Value(
        field_type=Rule.TIMEOUT,
        display_order=3,
        description="Turn-off delay",
        default=30000,
        min_count=0,
        max_count=1
    )

    def __init__(self):
        super().__init__()
        # These instance attributes come from Rule.__init__().
        self.name = None
        self.description = None
        self.summary = None
        self.category = None

        # Runtime-only rule state belongs here.
        self._timer = None
        self._cache = {}
```

In this shape:

- `type`, `name`, `description`, and `summary` are the static class attributes used for template generation
- `sensors`, `devices`, and `timeout` are static descriptor definitions copied into the generated template


#### Selector and field-type constants

The `Rule` base class exposes convenience constants that are commonly used as `field_type` values in `Input`, `Output`,
and `Value` declarations.

Common actor and selector constants:

<table>
<thead><tr><th>Rule constant</th><th>Value</th><th>Typical use</th></tr></thead>
<tbody>
<tr><td><code>Rule.DEVICE</code></td><td><code>"DEVICE"</code></td><td>Generic device selector.</td></tr>
<tr><td><code>Rule.MOTION_SENSOR</code></td><td><code>"MOTION"</code></td><td>Motion-sensor input/output selector.</td></tr>
<tr><td><code>Rule.CONTACT_SENSOR</code></td><td><code>"CONTACT"</code></td><td>Contact-sensor selector.</td></tr>
<tr><td><code>Rule.POWER_SOCKET</code></td><td><code>"POWER_SOCKET"</code></td><td>Switch/socket selector.</td></tr>
<tr><td><code>Rule.LIGHT</code></td><td><code>"LIGHT"</code></td><td>Light selector.</td></tr>
<tr><td><code>Rule.RC</code></td><td><code>"REMOTE_CONTROL"</code></td><td>Remote-control selector.</td></tr>
<tr><td><code>Rule.DOORBELL</code></td><td><code>"DOORBELL"</code></td><td>Doorbell selector.</td></tr>
<tr><td><code>Rule.TWILIGHT</code></td><td><code>"TWILIGHT"</code></td><td>Twilight/lux-style selector.</td></tr>
<tr><td><code>Rule.SMOKE_ALARM</code></td><td><code>"SMOKE_ALARM"</code></td><td>Smoke-alarm selector.</td></tr>
<tr><td><code>Rule.MOISTURE_SENSOR</code></td><td><code>"MOISTURE"</code></td><td>Moisture/flood selector.</td></tr>
<tr><td><code>Rule.SIGNAL</code></td><td><code>"SIGNAL"</code></td><td>Signal device selector.</td></tr>
<tr><td><code>Rule.SETPOINT</code></td><td><code>"SETPOINT_TEMPERATURE"</code></td><td>Setpoint-device selector.</td></tr>
<tr><td><code>Rule.VALVE</code></td><td><code>"VALVE"</code></td><td>Valve selector.</td></tr>
<tr><td><code>Rule.POWER_METER</code></td><td><code>"POWER_METER"</code></td><td>Power-meter selector.</td></tr>
<tr><td><code>Rule.USER</code></td><td><code>"HUB_USER"</code></td><td>User selector.</td></tr>
<tr><td><code>Rule.UI</code></td><td><code>"UI_DEVICE"</code></td><td>UI/mobile device selector.</td></tr>
<tr><td><code>Rule.MEDIA_RENDERER</code></td><td><code>"MEDIA_RENDERER"</code></td><td>Media-renderer selector.</td></tr>
<tr><td><code>Rule.RULE</code></td><td><code>"RULE"</code></td><td>Rule selector.</td></tr>
<tr><td><code>Rule.SCENE</code></td><td><code>"SCENE"</code></td><td>Scene selector.</td></tr>
<tr><td><code>Rule.GROUP</code></td><td><code>"GROUP"</code></td><td>Group selector.</td></tr>
<tr><td><code>Rule.ROOM</code></td><td><code>"ROOM"</code></td><td>Room selector.</td></tr>
<tr><td><code>Rule.ZONE</code></td><td><code>"ZONE"</code></td><td>Zone selector.</td></tr>
<tr><td><code>Rule.HUB</code></td><td><code>"HUB"</code></td><td>Hub-scoped selector.</td></tr>
</tbody></table>

Extra-value and timer constants:

<table>
<thead><tr><th>Rule constant</th><th>Value</th><th>Typical use</th></tr></thead>
<tbody>
<tr><td><code>Rule.TIMEOUT</code></td><td><code>"TIME_DELTA_MS"</code></td><td>Timeout/delay extra values.</td></tr>
<tr><td><code>Rule.FREE_TEXT</code></td><td><code>"FREE_TEXT"</code></td><td>Arbitrary text extras.</td></tr>
<tr><td><code>Rule.PASSWORD</code></td><td><code>"PASSWORD"</code></td><td>Password or secret input.</td></tr>
<tr><td><code>Rule.NUMBER</code></td><td><code>"NUMBER"</code></td><td>Numeric extra values.</td></tr>
<tr><td><code>Rule.PERCENT</code></td><td><code>"PERCENT"</code></td><td>Percent values.</td></tr>
<tr><td><code>Rule.CELSIUS</code></td><td><code>"CELSIUS"</code></td><td>Temperature values.</td></tr>
<tr><td><code>Rule.PPM</code></td><td><code>"PPM"</code></td><td>PPM values.</td></tr>
<tr><td><code>Rule.BOOL</code></td><td><code>"BOOL"</code></td><td>Boolean extras.</td></tr>
<tr><td><code>Rule.OPTION</code></td><td><code>"OPTION"</code></td><td>Option-list selection.</td></tr>
<tr><td><code>Rule.BUTTON</code></td><td><code>"BUTTON_ID"</code></td><td>Remote/button-id value.</td></tr>
<tr><td><code>Rule.EMAIL</code></td><td><code>"EMAIL"</code></td><td>Email address input.</td></tr>
<tr><td><code>Rule.TIMED_PRESET</code></td><td><code>"TIMED_PRESET"</code></td><td>Timed state presets.</td></tr>
<tr><td><code>Rule.TIMER</code></td><td><code>"RULETIME"</code></td><td><code>RULETIME</code>-based schedule values.</td></tr>
<tr><td><code>Rule.USER_NOTIFICATION</code></td><td><code>"USER_NOTIFICATION"</code></td><td>Notification-recipient extras.</td></tr>
</tbody></table>

Other useful base-class constants:

<table>
<thead><tr><th>Rule constant</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>Rule.FI</code>, <code>Rule.EN</code>, <code>Rule.LANG_DEFAULT</code></td><td>Language-code constants for localized dictionaries.</td></tr>
<tr><td><code>Rule.AND_OP</code>, <code>Rule.OR_OP</code></td><td>Capability-combining operations for selector fields.</td></tr>
<tr><td><code>Rule.ALWAYS_ON</code></td><td>On-hint constant for rule proposals.</td></tr>
<tr><td><code>Rule.HOME_SCENE</code>, <code>Rule.AWAY_SCENE</code></td><td>Scene-icon/default identifiers used by some templates and helpers.</td></tr>
<tr><td><code>Rule.FILTER_ALL</code>, <code>Rule.FILTER_OWNER</code>, <code>Rule.FILTER_HOME_SCENE</code>, <code>Rule.FILTER_EXISTENCE</code>, <code>Rule.FILTER_ADMIN_NOTIFICATION</code></td><td>Default filter class names that may be used in descriptor definitions.</td></tr>
</tbody></table>

#### Descriptor classes used by rule classes

Rule classes declare their selectable fields with `Input`, `Output`, and `Value`. These all inherit the common
configuration shape from `RuleConfigurationItem`.

Common `RuleConfigurationItem` fields:

<table>
<thead><tr><th>Field</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>fieldType</code></td><td>Logical field type, usually one of the `Rule.*` constants above.</td></tr>
<tr><td><code>displayOrder</code></td><td>Ordering hint for the generated template/UI.</td></tr>
<tr><td><code>description</code></td><td>User-facing label or description.</td></tr>
<tr><td><code>help</code></td><td>Optional helper text.</td></tr>
<tr><td><code>minCount</code>, <code>maxCount</code></td><td>Cardinality constraints.</td></tr>
<tr><td><code>minValue</code>, <code>maxValue</code></td><td>Value constraints for typed extras.</td></tr>
<tr><td><code>defaultValue</code></td><td>Default value shown in the template.</td></tr>
<tr><td><code>readOnly</code></td><td>Whether the field is effectively read-only in the template.</td></tr>
<tr><td><code>priority</code></td><td>Selection/UI prioritization hint.</td></tr>
<tr><td><code>filter</code></td><td>Optional filter-class hint.</td></tr>
</tbody></table>

`Input` and `Output` add:

<table>
<thead><tr><th>Field</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>capabilities</code></td><td>Required capability list for selected objects.</td></tr>
<tr><td><code>selectOp</code></td><td>Capability-combining operation, normally <code>Rule.AND_OP</code> or <code>Rule.OR_OP</code>.</td></tr>
<tr><td><code>icon</code></td><td>Optional icon hint.</td></tr>
</tbody></table>

Capability matching is exact. Capability names are unique keys, not a hierarchy. A selector that requires `MOTION` matches objects with capability `MOTION`; it does not implicitly match `SELF_MOTION`.

`Value` adds:

<table>
<thead><tr><th>Field</th><th>Meaning</th></tr></thead>
<tbody>
<tr><td><code>options</code></td><td>Selectable option list used with option-type extras.</td></tr>
<tr><td><code>extraFor</code></td><td>Optional link to another field this extra belongs to.</td></tr>
</tbody></table>

`fieldType` guidance:

- set `fieldType` only when it is actually needed
- for many device selectors, `capabilities=[...]` is enough and `field_type=None` is normal in current rules
- add `fieldType` when the rule needs a specific object class that capabilities alone do not identify reliably, or when the field is not selecting an ordinary device at all

Non-device selector field types currently used by factory rules include:

<table>
<thead><tr><th>Field type</th><th>What it selects</th><th>Examples in current rules</th></tr></thead>
<tbody>
<tr><td><code>Rule.SCENE</code></td><td>Scene ids.</td><td><code>scene_on_scene</code>, <code>scene_max_time_on</code>, <code>scene_on_movement</code>, <code>timed_scene</code></td></tr>
<tr><td><code>Rule.ROOM</code></td><td>Room ids.</td><td><code>automate_switches</code></td></tr>
<tr><td><code>Rule.UI</code></td><td>UI/mobile-presence objects.</td><td><code>notify_on_ui</code>, <code>sceneonuserpresence</code>, <code>home_away</code></td></tr>
</tbody></table>

Other specific selector field types currently used by factory rules include:

<table>
<thead><tr><th>Field type</th><th>Why it is used</th><th>Examples in current rules</th></tr></thead>
<tbody>
<tr><td><code>Rule.RC</code></td><td>The field must select remote-control devices specifically. Wall-switch style button devices use the same button-selection model when paired with <code>BUTTON_ID</code> values.</td><td><code>ikea_rc_emulation</code>, <code>hue_switch_emulation</code>, <code>scene_rotate_button</code></td></tr>
<tr><td><code>Rule.MEDIA_RENDERER</code></td><td>The field must select media-renderer devices.</td><td><code>autosound</code>, <code>lightshow</code></td></tr>
<tr><td><code>Rule.LIGHT</code></td><td>The field must select light devices specifically.</td><td><code>autolight</code>, <code>timedlight</code>, <code>light_loop_colors</code></td></tr>
<tr><td><code>Rule.POWER_SOCKET</code></td><td>The field must select socket/switch devices specifically.</td><td><code>autoswitch</code>, <code>multirule</code></td></tr>
<tr><td><code>Rule.DEVICE</code></td><td>The field accepts generic devices when no tighter selector is appropriate.</td><td><code>scene_on_device</code>, <code>deviceguard</code></td></tr>
</tbody></table>

#### Template generation from a rule class

`GET /rules/templates` does not read a separately authored template file for a rule. The Hub generates a `RuleTemplate`
from the rule class itself.

Template generation rules:

- the class attribute `type` becomes `RuleTemplate.configType`
- the class attributes `name`, `description`, and `summary` become the user-facing template metadata
- class attributes of type `Input` are copied into `RuleTemplate.inputs`
- class attributes of type `Output` are copied into `RuleTemplate.outputs`
- class attributes of type `Value` are copied into `RuleTemplate.extras`
- localized strings in names, descriptions, summaries, field descriptions, and field help are resolved in the request language
- other public non-callable class attributes are copied into the template object as additional metadata

The full serialized `RuleTemplate` and `RuleConfig` data model is documented in section 6.10.

#### Runtime binding model

When the Hub instantiates a rule:

- each configured `Input` field becomes an instance attribute whose value is a list of selected object ids
- each configured `Output` field becomes an instance attribute whose value is a list of selected object ids
- each configured `Value` field becomes an instance attribute whose value is a list of typed value objects
- a single-value extra is therefore still accessed as a list element such as `self.timeout[0].value`
- the configured rule name is also available through the runtime instance

The behavioral hook methods and bound helper methods provided by the `Rule` base class are documented in sections 15.5 and 15.8.

### 15.4 Commands sent from rules

Custom rules send commands to the Hub with `self.send_command(cmd)`.

The same path is used by factory rules and custom rules. The command object is sent into the normal Hub command-processing
flow, so the object must match the same message classes and attributes that the Hub expects elsewhere in the API.

Commands that are already documented elsewhere in this API reference can also be sent from rules. In particular:

- `DeviceCommand`, `DeviceOnCommand`, and `DeviceOffCommand` are documented in the device data chapter at section 6.8
- `SceneOnCommand` and `SceneOffCommand` are documented in the scene data chapter at section 6.11
- `RecordOnCommand` and `RecordOffCommand` are documented in the camera/video chapter at section 14.6

This section therefore documents only command objects that are primarily relevant to rules and not otherwise described in detail elsewhere in this document.

#### Status refresh command

`ReportStatusCommand` requests a fresh status report from one actor. Factory rules use it heavily during `enable()` and
other initialization paths so the rule can cache current names, states, or metadata before later events arrive.

Serialized type:

- `CMD_RPT_STATUS`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target actor id.</td><td>string</td></tr>
</tbody></table>

#### Notification commands

`NotifyCommand` sends a message to one mobile/UI target. `NotifyUserCommand` targets a Hub user instead of one mobile device.

Serialized types:

- `NOTIFY_CMD`
- `NOTIFY_USER_CMD`

`NotifyCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Mobile or UI device id.</td><td>string</td></tr>
<tr><td><code>msgId</code></td><td>Optional message id.</td><td>string or null</td></tr>
<tr><td><code>msg</code></td><td>Notification text.</td><td>string</td></tr>
<tr><td><code>sms</code></td><td>Whether the same message should also be sent as SMS.</td><td>boolean or null</td></tr>
</tbody></table>

`NotifyUserCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>User id.</td><td>string</td></tr>
<tr><td><code>msgId</code></td><td>Optional message id.</td><td>string or null</td></tr>
<tr><td><code>sendSms</code></td><td>Whether SMS should be sent to the user's phone number.</td><td>boolean or null</td></tr>
<tr><td><code>sendEmail</code></td><td>Whether email should be sent to the user's email address.</td><td>boolean or null</td></tr>
<tr><td><code>sendNotification</code></td><td>Whether push notification should be sent to the user's mobile devices.</td><td>boolean or null</td></tr>
<tr><td><code>msg</code></td><td>Message text.</td><td>string or null</td></tr>
<tr><td><code>subject</code></td><td>Email subject.</td><td>string or null</td></tr>
<tr><td><code>extraSms</code></td><td>Extra phone numbers for SMS delivery.</td><td>array of string or null</td></tr>
<tr><td><code>extraEmail</code></td><td>Extra email addresses for email delivery.</td><td>array of string or null</td></tr>
</tbody></table>

#### Email commands

`EmailCommand` sends an email. `EmailSnapShotCmd` sends a camera snapshot by email.

Serialized types:

- `EMAIL_CMD`
- `CMD_EMAIL_SNAPSHOT`

`EmailCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>msgId</code></td><td>Optional message id.</td><td>string or null</td></tr>
<tr><td><code>email</code></td><td>Recipient email address.</td><td>string</td></tr>
<tr><td><code>subject</code></td><td>Email subject.</td><td>string or null</td></tr>
<tr><td><code>msg</code></td><td>Email body.</td><td>string or null</td></tr>
<tr><td><code>attachments</code></td><td>Optional attachment list.</td><td>array or null</td></tr>
</tbody></table>

`EmailSnapShotCmd` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Camera device id.</td><td>string</td></tr>
<tr><td><code>email</code></td><td>Recipient email address.</td><td>string</td></tr>
<tr><td><code>body</code></td><td>Optional email body text.</td><td>string or null</td></tr>
</tbody></table>

#### Alert command

`AlertCommand` triggers an alert-capable device such as a siren or flashing light.

Serialized type:

- `CMD_ALERT`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target alert-capable device id.</td><td>string</td></tr>
<tr><td><code>high_priority</code></td><td>Whether the alert should be treated as high priority.</td><td>boolean</td></tr>
<tr><td><code>alert_type</code></td><td>Optional alarm type identifier.</td><td>string or null</td></tr>
<tr><td><code>flash</code></td><td>Whether the target device should flash if supported.</td><td>boolean</td></tr>
</tbody></table>

#### Generic alarm event

`GenericAlarmEvent` can be sent by a rule to create a custom alarm flow for another rule or integration path.

`GenericAlarmEvent` serialized type:

- `G_ALARM_EVENT`

`GenericAlarmEvent` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Actor id associated with the alarm.</td><td>string</td></tr>
<tr><td><code>state</code></td><td>Alarm state object created with <code>create_general_alarm_in_state()</code>.</td><td><code>DeviceState</code>-shaped object</td></tr>
</tbody></table>

#### Dimming and color-loop commands

These commands are used by remote-control and lighting rules.

Serialized types:

- `CMD_DIM`
- `CMD_STOP_DIM`
- `CMD_COLOR_LOOP`

`DimCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target light id.</td><td>string</td></tr>
<tr><td><code>step</code></td><td>Relative dimming step.</td><td>number or null</td></tr>
<tr><td><code>transitionMsec</code></td><td>Optional dimming transition duration.</td><td>integer or null</td></tr>
<tr><td><code>onoff</code></td><td>Optional on/off intent associated with the dim command.</td><td>boolean or null</td></tr>
<tr><td><code>force</code></td><td>Optional force flag.</td><td>boolean or null</td></tr>
</tbody></table>

`StopDimCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target light id.</td><td>string</td></tr>
</tbody></table>

`ColorLoopCommand` fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target light id.</td><td>string</td></tr>
<tr><td><code>start</code></td><td>Whether color looping should start or stop.</td><td>boolean or null</td></tr>
</tbody></table>

#### Media-renderer commands

`AVNextCommand` and `AVPreviousCommand` are simple media-renderer transport commands.

Serialized types:

- `CMD_AV_NEXT`
- `CMD_AV_PREVIOUS`

Fields:

<table>
<thead><tr><th>Field</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>Target media-renderer id.</td><td>string</td></tr>
</tbody></table>

### 15.5 Rule source format

`POST /rules/template` accepts the custom rule as Python source code in the `ruleLogic` query parameter.

Parser rules enforced by the Hub:

- the source must contain exactly one top-level class
- no other top-level statements are allowed
- the class must derive from `Rule`
- the class must override `type`
- the class must override `name`
- the class must override `description`
- the class must override `summary`
- `description` and `summary` may be either plain strings or localized dictionaries
- the rule type must be unique among installed rules
- a custom rule may replace an older custom rule with the same type
- a custom rule may not replace a factory rule type

Important parser behavior:

- Python `import` and `from ... import ...` statements are stripped before execution
- the Hub executes the class in a restricted environment that already exposes `Rule`, `Input`, `Output`, `Value`,
  `RuleTime`, `CronDef`, relevant command/event/state classes, standard built-ins, and helper modules such as `json`,
  `time`, `datetime`, `base64`, and `ElementTree`
- after successful parsing, the Hub forces the installed rule category to `CUSTOM_RULE`

Authoring model:

- the custom rule is authored as one Python class derived from `Rule`
- public class attributes of type `Input`, `Output`, and `Value` define the rule template shown by `GET /rules/templates`
- the class attribute `type` becomes `RuleTemplate.configType`
- the class attributes `name`, `description`, and `summary` become the user-facing template metadata
- other public non-callable class attributes are also copied into the template object

Base-class callbacks and helpers:

- `enable(self)` is called when the rule is turned on
- `disable(self)` is called when the rule is turned off
- `migrate_config(config, template)` is a static hook called before the runtime instance is created and configured
- `config` is the mutable persisted `RuleConfig` being loaded
- `template` is the current `RuleTemplate` generated from the installed rule class
- `migrate_config()` is used to rewrite older stored config shapes into the current expected form
- `on_config(self)` is called after configuration has been bound onto the rule instance
- `on_delete(self)` is called when the rule instance is deleted
- `on_event(self, ev)` handles runtime events for configured inputs and any explicitly registered output events
- for ordinary device events, `ev` is a `DeviceStateMessage` or one of its subclasses, as documented in section 15.2
- a device status update may also reach `on_event()` because the Hub converts `status.status.state` into a synthetic `DeviceStateMessage`
- for user-session-based rules, `ev` may instead be `UserSessionEvent`, also documented in section 15.2
- `on_status(self, status)` handles status snapshots for configured inputs
- `status` is the device status object from `DeviceStatusMessage.status`, not the whole wrapper message
- for a normal device status update, `on_status(status)` and `on_event(DeviceStateMessage(status.id, status.state))` may both be called
- `on_alert(self, ev)` handles Hub alerts delivered to the rule
- `ev` is an `Alert` object as documented in section 15.2
- typical use case: change a signal state indicating alert in a set of devices. The signal state is then typically reported to external systems.
- `on_generic_alert_from_rule(self, ev)` handles rule-originated generic alarm events
- `ev` is a `GenericAlarmEvent`, documented in section 15.2
- typical use case: change a signal state indicating alert in a set of devices. The signal state is then typically reported to external systems.
- `on_scene(self, scene_id, on)` handles scene status changes relevant to the rule
- `scene_id` is the scene id string and `on` is the current boolean scene state
- `on_timeout(self, timer_id)` handles custom one-shot timers started with `start_timer()`
- `timer_id` is the timer id string returned by `start_timer()`
- `on_ruletimer(self, begin, name)` handles configured `RULETIME` extras
- `begin` is `True` when a configured rule-time period starts and `False` when it ends
- `name` is the extra-field name whose `RULETIME` list fired

Common implementation patterns in existing rules:

- factory rules often implement `enable()` to request fresh status from important inputs with `ReportStatusCommand`
- `migrate_config()` is used to rewrite older persisted config shapes before binding
- `on_config()` is used to backfill defaults or derive cached runtime values from the already bound config
- `on_status()` is often used to cache names or metadata needed later in `on_event()`
- `on_event()` is the main place where automation logic reacts to incoming state changes
- `on_timeout()` is used together with `start_timer()` and `stop_timer()` for delayed off/on behavior

Examples of overridable methods:

```python
class ExampleRule(Rule):
    type = "EXAMPLE_RULE"
    name = "Example rule"
    description = "Example rule description"
    summary = "Example rule summary"

    sensors = Input(
        capabilities=[Capability.MOTION],
        display_order=1,
        description="Motion sensor",
        min_count=1
    )

    devices = Output(
        capabilities=[Capability.ON_OFF],
        display_order=2,
        description="Controlled device",
        min_count=1
    )

    timeout = Value(
        field_type=Rule.TIMEOUT,
        display_order=3,
        description="Turn off delay",
        default=30000,
        min_count=0,
        max_count=1
    )

    def __init__(self):
        super().__init__()
        self._timer = None
        self._names = {}

    @staticmethod
    def migrate_config(config, template):
        # Called before the runtime rule instance exists.
        # Use this to normalize old saved config into the current template shape.
        if "delay" in config.extras and "timeout" not in config.extras:
            config.extras["timeout"] = config.extras.pop("delay")

    def on_config(self):
        # Called after self.sensors, self.devices, and self.timeout have been bound.
        self._timeout_ms = self.timeout[0].value if self.timeout else 30000

    def enable(self):
        # Called when the rule is turned on.
        for sensor in self.sensors:
            self.send_command(ReportStatusCommand(sensor))

    def disable(self):
        # Called when the rule is turned off.
        if self._timer:
            self.stop_timer(self._timer)
            self._timer = None

    def on_status(self, status):
        # Useful for caching names or metadata.
        self._names[status.id] = status.name

    def on_event(self, ev):
        # Main event callback for configured inputs.
        if ev.state.motion:
            for device in self.devices:
                self.send_command(DeviceOnCommand(device))
            self._timer = self.start_timer(self._timeout_ms, self._timer)

    def on_timeout(self, timer_id):
        # Fired for timers started with start_timer().
        self._timer = None
        for device in self.devices:
            self.send_command(DeviceOffCommand(device))

    def on_scene(self, scene_id, on):
        # Optional callback for scene changes affecting the rule.
        self.logger.info("Scene %s changed to %s", scene_id, on)

    def on_alert(self, alert):
        # alert is an Alert object.
        self.logger.info("Alert from %s", alert.sourceId)

    def on_generic_alert_from_rule(self, ev):
        # ev is a GenericAlarmEvent with ev.id and ev.state.
        self.logger.info("Generic alarm from rule for %s", ev.id)

    def on_ruletimer(self, begin, name):
        # begin tells whether the configured RULETIME period started or ended.
        self.logger.info("RULETIME %s changed to %s", name, begin)

    def on_delete(self):
        # Called when the rule instance is removed.
        self.logger.info("Rule deleted")
```

Example custom rule:

```python
class OnFromMovement(Rule):
    type = "ON_FROM_MOVEMENT_RULE"
    name = "Turn on with movement"
    description = "Put device on when movement."
    summary = "When motion sensor reports movement, put device on. If movement stops, put device off."

    sensors = Input(
        field_type=None,
        capabilities=[Capability.MOTION],
        display_order=1,
        description="Motion sensor",
        min_count=1
    )

    devices = Output(
        field_type=None,
        capabilities=[Capability.ON_OFF],
        display_order=2,
        description="Controlled device",
        min_count=1
    )

    def send_commands(self, on: bool):
        for device in self.devices:
            if on:
                self.send_command(DeviceOnCommand(device))
            else:
                self.send_command(DeviceOffCommand(device))

    def on_event(self, ev):
        self.send_commands(ev.state.motion)
```

This example reflects the same structure used by the built-in rule set:

- `sensors` and `devices` are declared as class-level `Input` and `Output` descriptors
- at runtime, `self.sensors` and `self.devices` are lists of selected device ids
- the rule logic reacts in `on_event()` and dispatches ordinary Hub device commands with `send_command()`

### 15.6 Commands and examples

This section shows minimal Python snippets for the command objects documented in section 15.4.

Example: `ReportStatusCommand`

```python
sensor_id = self.sensors[0]

self.send_command(ReportStatusCommand(sensor_id))
```

Example: `NotifyCommand`

```python
mobile_id = self.mobiles[0]

self.send_command(NotifyCommand(mobile_id, "Front door opened", sms=False))
```

Example: `NotifyUserCommand`

```python
user_id = self.users[0]

cmd = NotifyUserCommand(user_id)
cmd.sendNotification = True
cmd.sendEmail = True
cmd.msg = "Security alarm active"
cmd.subject = "Cozify alarm"

self.send_command(cmd)
```

Example: `EmailCommand`

```python
self.send_command(
    EmailCommand(
        email="user@example.com",
        subject="Temperature warning",
        msg="Living room temperature is above the configured limit.",
        name=self.name
    )
)
```

Example: `EmailSnapShotCmd`

```python
camera_id = self.cameras[0]

self.send_command(
    EmailSnapShotCmd(camera_id, "user@example.com", "Motion detected at the front door.")
)
```

Example: `AlertCommand`

```python
sirene_id = self.alertees[0]

self.send_command(
    AlertCommand(sirene_id, high_priority=True, flash=True)
)
```

Example: `GenericAlarmEvent`

```python
device_id = self.sensors[0]

state = DeviceState()
state.gAlarm = True
state.gAlarmAt = self.get_time_ms()
state.gAlarmId = "custom-alarm-id"
state.gAlarmCode = "CUSTOM_THRESHOLD"
state.gAlarmMsg = "Custom threshold exceeded"

self.send_command(GenericAlarmEvent(device_id, state))
```

Example: `DimCommand`

```python
light_id = self.devices[0]

self.send_command(DimCommand(light_id, step=0.1, transition_msec=400, onoff=True))
```

Example: `StopDimCommand`

```python
light_id = self.devices[0]

self.send_command(StopDimCommand(light_id))
```

Example: `ColorLoopCommand`

```python
light_id = self.devices[0]

self.send_command(ColorLoopCommand(light_id, True))
```

Example: `AVNextCommand` and `AVPreviousCommand`

```python
player_id = self.players[0]

self.send_command(AVNextCommand(player_id))
self.send_command(AVPreviousCommand(player_id))
```

Notes:

- ids such as `self.sensors[0]`, `self.users[0]`, `self.mobiles[0]`, and `self.players[0]` come from the configured rule inputs and outputs
- ordinary device, scene, and camera control commands are documented outside chapter 15 and can be sent from rules in the same way

### 15.7 Custom-rule endpoints

#### Reading installed custom rule source

`GET /rules/template?ruleType=<rule-type>` returns the stored source code for one installed custom rule type.

Behavior:

- returns the raw source string stored for that custom rule type
- returns not found when the requested rule type is unknown
- does not expose factory-rule implementation source

Example request:

```http
GET /cc/<api-version>/rules/template?ruleType=MY_CUSTOM_RULE
```

#### Installing or replacing a custom rule

`POST /rules/template?ruleLogic=<python-source>` installs a custom rule.

Behavior:

- parses the supplied source and validates the class structure
- if the same custom rule type already exists, the old custom rule is uninstalled first
- if the same type belongs to a factory rule, installation fails
- on success, returns the installed rule type string
- on parse failure, returns an error

Example success value:

```json
"MY_CUSTOM_RULE"
```

#### Reading custom rule logs

`GET /rules/log?ruleType=<rule-type>` returns the runtime log text for one custom rule type.

Behavior:

- reads the current log file for that rule type
- if one rotated backup log exists, it is concatenated before the current log
- if no log files exist, the endpoint returns an empty string

#### Uninstalling a custom rule

`DELETE /rules/template?ruleType=<rule-type>` removes one custom rule type.

Behavior:

- removes the persisted source code
- removes the custom rule class from the available template set
- clears the custom rule's log files
- disables and removes all existing rule instances whose `configType` matches the removed custom rule type
- returns `true` on success

### 15.8 Runtime behavior

After installation, a custom rule is exposed through the same template system as any other rule:

- `GET /rules/templates` returns it as a normal `RuleTemplate`
- `RuleTemplate.category` is `CUSTOM_RULE`
- `RuleTemplate.configType` is the custom class `type`
- localized `name`, `description`, `summary`, field descriptions, and field help are resolved using the request language

Custom rules also use the normal rule instance model:

- create or update instances with `PUT /rules`
- change on-configuration with `PUT /rules/onConfiguration`
- turn instances on or off with `PUT /rules/command`
- remove instances with `DELETE /rules`

The runtime binds helper methods from the Hub onto the `Rule` instance.

Helper API:

<table>
<thead><tr><th>Method</th><th>Behavior</th><th>Notes</th></tr></thead>
<tbody>
<tr><td><code>send_command(cmd, role=None)</code></td><td>Sends a Hub command from the rule to the normal command-processing path.</td><td>Used for ordinary device, scene, rule, notification, email, and alert commands. The Hub executes the command in a system-rule context. When <code>role=ANONYMOUS</code> is used, the command bypasses normal reachability-style restrictions intended for selected remote-control flows.</td></tr>
<tr><td><code>start_timer(timeout_ms, timer=None)</code></td><td>Starts a custom one-shot timer and returns its timer id.</td><td>If <code>timer</code> refers to an existing timer created by the same rule, that timer is cancelled and replaced. The returned timer id is delivered later to <code>on_timeout(timer_id)</code>.</td></tr>
<tr><td><code>stop_timer(timer=None)</code></td><td>Stops a previously started custom timer.</td><td>Use the timer id returned by <code>start_timer()</code>. Stopping an unknown timer is tolerated.</td></tr>
<tr><td><code>make_request(method, url, cb, data=None, headers=None)</code></td><td>Performs an asynchronous HTTP request and calls the supplied callback with the HTTP response object.</td><td>The request body is truncated to at most 50&nbsp;000 bytes by the runtime wrapper. The callback is executed only while the rule is still on. Callback execution still runs under the same watchdog protection as other custom-rule callbacks.</td></tr>
<tr><td><code>hub_name()</code></td><td>Returns the current Hub name.</td><td>Useful when rules compose user-facing notification or email messages that should identify the installation.</td></tr>
<tr><td><code>get_factory_scene(icon)</code></td><td>Returns the scene id of a factory scene matching the given icon identifier.</td><td>Returns <code>null</code> when no matching factory scene exists.</td></tr>
<tr><td><code>get_scene_name(scene_id)</code></td><td>Returns the current name of the given scene.</td><td>Returns <code>null</code> when the scene does not exist.</td></tr>
<tr><td><code>get_hub_type()</code></td><td>Returns the configured house or Hub type.</td><td>This is read from Hub user settings.</td></tr>
<tr><td><code>get_hub_language()</code></td><td>Returns the configured Hub language code.</td><td>This is the Hub-side user-settings language, not necessarily the language of the current API request.</td></tr>
<tr><td><code>get_time_ms()</code></td><td>Returns the current timestamp in milliseconds.</td><td>Useful for timer comparisons, event timestamps, and constructing state payloads such as generic alarm events.</td></tr>
<tr><td><code>get_localtime(ts)</code></td><td>Formats a millisecond timestamp using the Hub timezone and returns a human-readable local-time string.</td><td>The current implementation returns a string formatted like <code>dd.mm.yyyy HH:MM:SS</code>.</td></tr>
<tr><td><code>get_unique_user_mobiles(mobiles)</code></td><td>Deduplicates a list of UI/mobile device ids so that only one mobile device per user remains.</td><td>Useful when a rule wants to notify users instead of every physical mobile device separately.</td></tr>
<tr><td><code>set_persisted_data(key, value)</code></td><td>Stores rule-private persisted data under a string key.</td><td>The key must be a non-empty string. The value must be JSON-serializable. Stored data survives Hub restarts and rule reconfiguration for that same rule instance.</td></tr>
<tr><td><code>get_persisted_data(key)</code></td><td>Reads previously stored rule-private persisted data.</td><td>Returns <code>null</code> when the key does not exist.</td></tr>
<tr><td><code>remove_persisted_data(key)</code></td><td>Removes one persisted data entry for the current rule instance.</td><td>Does nothing if the key is absent.</td></tr>
<tr><td><code>clear_persisted_data()</code></td><td>Removes all persisted data entries for the current rule instance.</td><td>Useful when a rule wants to reset its own learned runtime state.</td></tr>
<tr><td><code>register_for_outputs_events(item_ids)</code></td><td>Registers selected output items so the rule also receives their events in <code>on_event()</code>.</td><td>Normally rules receive events from inputs only. This helper is for rules that need feedback from output devices they control.</td></tr>
<tr><td><code>get_power_prices(cb)</code></td><td>Asynchronously reads current power-price data and calls the supplied callback with the result.</td><td>The callback-based shape matches the rest of the custom-rule runtime. It is intended for spot-price-aware rules.</td></tr>
</tbody></table>

Examples of helper usage:

```python
class HelperExamples(Rule):
    type = "HELPER_EXAMPLES_RULE"
    name = "Helper examples"
    description = "Shows how Hub helper methods are used from a custom rule."
    summary = "Shows scene lookup, persistence, async HTTP, output feedback, and notifications."

    button = Input(
        capabilities=[Capability.ON_OFF],
        display_order=1,
        description="Trigger button",
        min_count=1,
        max_count=1
    )

    lamp = Output(
        capabilities=[Capability.ON_OFF],
        display_order=2,
        description="Feedback lamp",
        min_count=1,
        max_count=1
    )

    def on_config(self):
        # Receive events also from selected outputs.
        self.register_for_outputs_events(self.lamp)

    def on_event(self, ev):
        # Toggle a device.
        self.send_command(DeviceOnCommand(self.lamp[0]))

        # Start a delayed follow-up action.
        self.start_timer(5000)

        # Persist rule-private state.
        count = self.get_persisted_data("press_count") or 0
        self.set_persisted_data("press_count", count + 1)

        # Resolve a factory scene.
        home_scene_id = self.get_factory_scene(Rule.HOME_SCENE)
        if home_scene_id:
            self.logger.info("Home scene id is %s", home_scene_id)

        # Send an HTTP request asynchronously.
        self.make_request(
            "GET",
            "https://example.invalid/status",
            self.on_http_done
        )

    def on_timeout(self, timer_id):
        self.send_command(DeviceOffCommand(self.lamp[0]))

    def on_http_done(self, response):
        self.logger.info("HTTP status %s", response.code)

    def notify_user(self, mobile_ids):
        unique_mobiles = self.get_unique_user_mobiles(mobile_ids)
        local_time = self.get_localtime(self.get_time_ms())
        hub_language = self.get_hub_language()
        hub_type = self.get_hub_type()

        for mobile in unique_mobiles:
            self.send_command(
                NotifyCommand(
                    mobile,
                    f"{self.hub_name()} {hub_type} {hub_language} {local_time}"
                )
            )
```

Runtime safety behavior:

- custom rule callbacks are executed under a watchdog timeout of 2 seconds
- if a custom rule blocks, the Hub logs the failure, disables that rule instance, and emits a `RULE_BLOCKED` user alert
- rule-specific runtime logs are written per rule type and exposed through `GET /rules/log`
