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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "fweinb/ha-sg-150",
"image": "mcr.microsoft.com/devcontainers/python:3.14-trixie",
"postCreateCommand": "scripts/setup",
"appPort": ["8123", "50600:50600/udp"],
"appPort": ["8123", "50600:50600/udp", "5004-5013:5004-5013/udp"],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
Expand All @@ -11,6 +11,10 @@
"50600": {
"label": "SG-150 SIP Port",
"onAutoForward": "notify"
},
"5004-5013": {
"label": "SG-150 RTP Ports",
"onAutoForward": "silent"
}
},
"customizations": {
Expand Down
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ updates:
# Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json
- dependency-name: "homeassistant"
# The test plugin pins one exact Home Assistant release, so it moves with it
- dependency-name: "pytest-homeassistant-custom-component"
- dependency-name: "pytest-home-assistant"
2 changes: 2 additions & 0 deletions .pytest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
filterwarnings = ['ignore:DeprecationWarning']
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
# Let Home Assistant install its httpx alias before respx is imported.
addopts = ["-p", "no:respx"]
112 changes: 67 additions & 45 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,67 @@
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Github is used for everything

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
4. Test you contribution.
5. Issue that pull request!

## Any contributions you make will be under the MIT Software License

In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project.

## Report bugs using Github's [issues](../../issues)

GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!

## Write bug reports with detail, background, and sample code

**Great Bug Reports** tend to have:

- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)

## License

By contributing, you agree that your contributions will be licensed under its MIT License.
# Contribution guidelines

Contributing to this project should be as easy and transparent as possible, whether it's:

- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features

## Github is used for everything

Github is used to host code, to track issues and feature requests, as well as accept pull requests.

Pull requests are the best way to propose changes to the codebase.

1. Fork the repo and create your branch from `main`.
2. If you've changed something, update the documentation.
3. Make sure your code lints (using `scripts/lint`).
4. Test you contribution.
5. Issue that pull request!

## Testing two-way audio on a real gateway

The tests cover the SIP, RTP and WebRTC code on fake transports and an in-process WebRTC peer, but not the real SG-150 or a real browser. Before releasing a change to call audio, go through this with a gateway:

1. Prepare:
- Home Assistant opened over HTTPS.
- **Two-way audio** turned on under Settings → System → Labs.
- **Two-way audio** turned on in the integration options.
- The RTP port range reachable from the gateway. In the devcontainer, 5004–5013/udp are forwarded.
2. With **SIP Auto Answer** off:
- Ring the door station. Every handset rings, and the camera's live view shows the talk button.
- Open the live view without pressing talk. The call keeps ringing.
- Press talk. The handsets stop ringing, and you hear the door station and it hears you.
- Close the live view. The call ends at the door station.
3. Ring again, and answer on a Siedle handset first. Pressing talk afterwards must not take over the call.
4. Ring again, and let nobody answer. After a minute the call is left to the handsets.
5. With **SIP Auto Answer** on, ring the door station:
- Audio from the door station plays in the live view.
- Closing the live view without having talked leaves the call up.
- The **Hang Up** button next to the camera ends it.
6. Run `play_audio` during a call. The clip plays at normal speed, without gaps.

## Any contributions you make will be under the MIT Software License

In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project.

## Report bugs using Github's [issues](../../issues)

GitHub issues are used to track public bugs.
Report a bug by [opening a new issue](../../issues/new/choose); it's that easy!

## Write bug reports with detail, background, and sample code

**Great Bug Reports** tend to have:

- A quick summary and/or background
- Steps to reproduce
- Be specific!
- Give sample code if you can.
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)

## License

By contributing, you agree that your contributions will be licensed under its MIT License.
93 changes: 72 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/header-dark.svg">
<img src="docs/header-light.svg" alt="SG-150 for Home Assistant: a door station, an indoor station, the gateway and Home Assistant on one In-Home bus" width="100%">
</picture>

# Siedle Gateway 150 Integration

[![HACS Default](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration)

This Home Assistant custom integration allowing integration with Siedle Gateway 150 (SG-150) via a local connection. It is developed to integrate a Siedle Door Camera with Home Assistant.

This integration will run a minimal SIP Server that can be configured in the SG-150 as a telephone device.

Currently the integration exposes:

- Gateway Information (Mac Adresse, SIP Connection Status)
- All phone devices know to the SG-150
- Phone number sensor
- All video extractors
- Camera for the live stream
- Media source with the pictures the gateway stored during calls, per PBX phone (Media → Siedle Gateway)
- Actions to control the SG-150:
- start_call: Start a phone call within the In-Home system
- stop_call: Stop a phone call within the In-Home system
- send_sip_info_dtmf: Sends a DTMF signal to the Siedle Gateway during an In-Home phone call
- get_active_calls: Returns a list of active call IDs (`active_calls`) and, in `calls`, each call's direction and the caller/callee device (`id`, `name`) when known.
- play_audio: Play a media_source_id to an active call (Supporting wav/mp3, see [Tips & Tricks](#tips--tricks))
- Every action takes an optional `config_entry_id` to pick the gateway; it can be omitted when only one gateway is set up.
- Gateway information: MAC address, SIP connection status and a light switch button
- All phone devices known to the SG-150, with their phone number
- Door stations, with a door opener button
- A camera for each video stream, with optional two-way audio (see [Two-way audio](#two-way-audio))
- A bell that rings when the door station calls Home Assistant
- A Hang Up button next to each camera
- A media source with the pictures the gateway stored during calls, per PBX phone (Media → Siedle Gateway)
- Actions to start and control calls (see [Actions](#actions))

# Disclaimer

Expand All @@ -38,16 +40,65 @@ While setting up the integration within Home Assistant you will be asked to prov
- Username (in most cases `admin`)
- Password (the one used for the web interface)

In addition to that the Home Assistant instance will also configure a SIP-Server, please make sure to setup the following values:
## Options

Home Assistant then sets up its own SIP server, which the SG-150 connects to like a telephone. You choose these options during setup, and can change them later with **Configure** on the integration:

| Option | Default | What it means |
|---|---|---|
| SIP Caller IP | | The IP address of your Home Assistant. The SG-150 uses it to reach Home Assistant. |
| SIP Username | | A name of your choosing. You enter the same name in the SG-150. |
| SIP Password | | A password of your choosing. You enter the same password in the SG-150. |
| SIP Number | | The phone number Home Assistant has in your In-Home system. |
| SIP Port | 5060 | The port the SG-150 connects to. |
| SIP Auto Answer | On | Whether Home Assistant answers when the door station calls. The camera only gets a picture during a call. While Home Assistant holds a call, your other indoor stations can't take it (see [Limitations](#limitations)). With two-way audio on, you can turn this off and answer from the camera instead. |
| Two-way audio | Off | Lets you hear and talk to the person at the door from the camera. See [Two-way audio](#two-way-audio). |
| First RTP Port / Last RTP Port | 5004 / 5013 | The ports used for the sound of calls, one per call at the same time. Only change them if they clash with something else. The SG-150 must be able to reach them, so allow them in your firewall if you have one. |

## Camera

Each video stream of the SG-150 becomes a camera in Home Assistant. In most setups it belongs to the TC phone you create for Home Assistant (see [below](#create-a-tc-phone-connected-to-the-sip-gateway)). The camera shows **Streaming** while there is a call with its device, and **Idle** otherwise.

Without two-way audio, the camera shows the picture only. With two-way audio on, its live view also plays the sound from the door station and has a talk button.

## Buttons

| Button | Where | What it does |
|---|---|---|
| Lightswitch | The gateway | Triggers the light switch of your In-Home system. |
| Door Opener | Each door station | Opens the door. |
| Hang Up | Next to each camera | Ends the call shown by that camera. If there is no call, Home Assistant shows an error. |

## Actions

These actions let automations and scripts start and control calls. Every action also takes an optional `config_entry_id` to pick the gateway; leave it out when only one gateway is set up.

| Action | What it does | Fields | Response |
|---|---|---|---|
| `sg_150.start_call` | Starts a call within the In-Home system, for example to a door station. | `to`: the number to call, including its prefix, without spaces | `call_id` of the new call |
| `sg_150.stop_call` | Ends a call. | `call_id` | `success` |
| `sg_150.send_sip_info_dtmf` | Sends a DTMF signal during a call, for example to trigger a function of the door station (see [Tips & Tricks](#tips--tricks)). | `call_id`, `signal` (0–9, `*` or `#`), `duration` in milliseconds (optional, default 100) | `success` |
| `sg_150.play_audio` | Plays a sound into a call, such as a text-to-speech message. Supports wav and mp3 (see [Tips & Tricks](#tips--tricks)). | `call_id`, `media_source_id` | `success` |
| `sg_150.get_active_calls` | Lists the calls in progress. | none | `active_calls`: the call IDs; `calls`: each call's direction and the devices on either end (`id`, `name`), when known |

If an action fails, its response contains an `error` with the reason.

## Two-way audio

With **Two-way audio** turned on, you can hear the door station and talk back from the camera's live view, like on an indoor station. This, as of writing, needs Home Assistant 2026.11 or later, and the following configuration:

1. Turn on the Labs feature under **Settings → System → Labs → Two-way audio**. Two-way audio is a preview feature, and Home Assistant only shows the talk button once it is turned on there.
2. Open Home Assistant over HTTPS. Browsers only let a page use your microphone on a secure connection.
3. Turn on **Two-way audio** in this integration's options.

The camera's live view then shows a talk button.

What happens when someone rings depends on the integration's **SIP Auto Answer** option:

- SIP Caller IP: `<HomeAssistant-IP>`
- SIP Username: `<NameOfYourChoosing>`
- SIP Password: `<PasswordOfYourChoosing>`
- SIP Number: `<PhoneNumberOfYourChoosing>`
- SIP Port: `<PortOfYourChoosing>` (default: 5060)
- SIP Auto Answer: `<AutoAnswerValue>` (default: True, determines if HA will automatically answer incoming calls from the SG-150 blocking the In-Home bus for other devices, needed so the video becomes available)
- **On:** Home Assistant answers straight away, as before. You hear the door station when you open the live view, and pressing talk lets them hear you.
- **Off:** Home Assistant rings along with your indoor stations. Pressing talk answers the call. If someone answers on an indoor station first, or nobody answers within a minute, Home Assistant leaves the call to them.

These values will be used to configure the SG-150 to connect to the SIP-Server provided by this integration.
The call ends when you close the live view you talked from, or when you press **Hang Up**. Just watching the live view never ends a call, except when the door station hangs up.

## Configure the SG-150 to connect to the SIP-Gateway

Expand Down
33 changes: 32 additions & 1 deletion custom_components/sg_150/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from typing import TYPE_CHECKING

import dns.rdata
import dns.rdataclass
import dns.rdatatype
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
Expand All @@ -18,17 +21,23 @@
from .const import (
API_PORT,
CONF_HOST_ADDRESS,
CONF_RTP_PORT_MAX,
CONF_RTP_PORT_MIN,
CONF_SIP_AUTO_ANSWER,
CONF_SIP_CALLER_IP,
CONF_SIP_NUMBER,
CONF_SIP_PASSWORD,
CONF_SIP_PORT,
CONF_SIP_USER,
CONF_TWO_WAY_AUDIO,
DEFAULT_RTP_PORT_MAX,
DEFAULT_RTP_PORT_MIN,
DOMAIN,
)
from .coordinator import SG150Coordinator
from .data import SG150RuntimeData
from .line import GatewayLine
from .media_source import SG150ImageView
from .services import async_setup_services
from .sip.minisip import MiniSIPServer

Expand All @@ -49,9 +58,21 @@
]


# Taken from https://github.com/home-assistant/core/pull/121702/changes
def load_dnspython_rdata_classes() -> None:
"""Load dnspython rdata classes used by aioice."""
# aioice's mDNS records use class IN with the cache-flush bit set, which
# dnspython caches separately from IN and would otherwise import on demand.
for rdclass in (dns.rdataclass.IN, dns.rdataclass.IN | 0x8000):
for rdtype in dns.rdatatype.RdataType:
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
dns.rdata.get_rdata_class(rdclass, rdtype) # type: ignore[no-untyped-call]


async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
"""Register the services once per Home Assistant instance."""
"""Register the services and the image view once per Home Assistant instance."""
async_setup_services(hass)
hass.http.register_view(SG150ImageView(hass))
return True


Expand All @@ -60,6 +81,9 @@ async def async_setup_entry(
entry: SG150ConfigEntry,
) -> bool:
"""Set up this integration using UI."""
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
await hass.async_add_executor_job(load_dnspython_rdata_classes)

coordinator = SG150Coordinator(hass=hass)

client = SG150ApiClient(
Expand All @@ -76,6 +100,10 @@ async def async_setup_entry(
call_host=entry.options.get(CONF_SIP_CALLER_IP),
users=users,
port=int(entry.options.get(CONF_SIP_PORT)),
rtp_ports=range(
int(entry.options.get(CONF_RTP_PORT_MIN, DEFAULT_RTP_PORT_MIN)),
int(entry.options.get(CONF_RTP_PORT_MAX, DEFAULT_RTP_PORT_MAX)) + 1,
),
)

line = GatewayLine(
Expand All @@ -84,6 +112,8 @@ async def async_setup_entry(
number=entry.options.get(CONF_SIP_NUMBER),
auto_answer=entry.options.get(CONF_SIP_AUTO_ANSWER, True),
devices=lambda: coordinator.data.devices if coordinator.data else [],
# Without auto answer, ring along so the camera's talk button can answer.
ring_along=entry.options.get(CONF_TWO_WAY_AUDIO, False),
)

entry.runtime_data = SG150RuntimeData(
Expand All @@ -94,6 +124,7 @@ async def async_setup_entry(
)

await coordinator.async_config_entry_first_refresh()
await coordinator.async_register_gateway_device()
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

await line.start()
Expand Down
Loading
Loading