You can take a look at my issue and pr queue if you are wondering why is something stale for days here
A Home Assistant custom integration that connects to a running
INDI server (indiserver, default TCP port 7624) as an additional
client, exactly the way CCDciel, KStars/EKOS or
indi_getprop/indi_setprop would. It does not take exclusive control of any device: it simply
reads whatever properties the server broadcasts (already reflecting changes made by other clients)
and, when a property allows it, can send its own commands to change it - bidirectionally.
Status: beta (
v1.1.0). The protocol core and entity mapping are functional and unit tested, but this has not yet been run against every INDI driver in the wild. Feedback and bug reports are very welcome.
Disclaimer: this is an independent, unofficial client. It is not affiliated with, endorsed by, or sponsored by the INDI Library project. "INDI" is used here only to describe protocol compatibility with
indiserver.
This integration is used in the author's private DevControl2 system (built for the Bombol.Space telescope hosting facility), but this integration itself is generic - it makes no assumption about any specific installation and works with any standard-compliant INDI driver/device.
Typical astronomy setups already run indiserver with drivers for the mount, camera, focuser,
filter wheel, dome, weather station, etc., controlled from an astronomy application (CCDciel,
KStars/EKOS, ...). This integration lets Home Assistant sit alongside that application as a peer
client, so you can, for example:
- Park/unpark the mount from an HA automation (e.g. on rain or high wind).
- Read and set a CCD's target temperature, and watch the current sensor temperature.
- See the CCD's latest captured frame as a camera preview, right in a Lovable dashboard.
- See device connection state and recent driver log messages inside HA.
- Trigger dome/roof or power-switch style properties from dashboards and automations.
INDI is a push-based, XML-over-TCP protocol: the client sends getProperties, the server answers
with def*Vector elements describing each device's properties, and later pushes set*Vector
elements whenever a value changes (caused by any connected client, or by the driver itself). This
integration implements that protocol directly in Python (custom_components/indi_client/indi/, no
pyindi-client/SWIG dependency - the protocol core itself is pure stdlib, only camera-frame
decoding needs numpy/Pillow), keeps a live model of every device/property, and maps them onto
Home Assistant entities automatically - there is no hardcoded list of "supported" devices or
drivers.
| INDI vector type | Permission | Rule | Home Assistant entity |
|---|---|---|---|
| Number | read-only | - | sensor (device_class temperature/humidity inferred by element name, e.g. *TEMPERATURE) |
| Number | writable | - | number |
| Text | read-only | - | sensor |
| Text | writable | - | text |
| Light | any | - | sensor (diagnostic; state is Idle/Ok/Busy/Alert) |
| Switch | writable | OneOfMany / AtMostOne |
select |
| Switch | read-only | OneOfMany / AtMostOne |
sensor (name of the active option) |
| Switch | writable | AnyOfMany |
switch per element |
| Switch | read-only | AnyOfMany |
binary_sensor per element |
CONNECTION (standard property) |
writable | - | switch named Connected (special-cased for a nicer toggle instead of a Connect/Disconnect dropdown) |
| BLOB (captured frame) | any | - | camera (see below) |
| device/server log messages | - | - | sensor Last message per device, with recent history in history attribute |
| server TCP link | - | - | diagnostic binary_sensor Server connected |
Each INDI device becomes its own Home Assistant device (grouping all of its entities), linked to a parent "INDI Server (host:port)" hub device.
So, concretely:
- Parking a mount - the standard
TELESCOPE_PARKswitch vector (ruleOneOfMany, optionsPARK/UNPARK) becomes aselectentity; pickingParksends the park command. - Setting CCD temperature -
CCD_TEMPERATUREbecomes anumberentity; changing it sends anewNumberVector, and the driver's actual sensor reading (if exposed as a separate read-only element) is asensor. - Checking logs - every device gets a diagnostic Last message sensor showing the latest INDI log line, with the last 25 messages available as an attribute.
- Connectivity - the
switch.<device>_connectedentity reflects and controls the driver'sCONNECTIONproperty, andbinary_sensor.server_connectedreflects the TCP link toindiserveritself.
BLOB (image) data is opt-in per the INDI protocol - a driver only sends it once a client asks. As
soon as a camera entity for a device's BLOB property (e.g. CCD1) is set up, this integration
sends that request (enableBLOB ... Also) automatically, so the camera starts showing the latest
captured frame with no extra configuration.
Frames are decoded with a small built-in FITS reader (the format virtually every INDI camera driver
uses), stretched with a percentile clip for a reasonable preview, and JPEG-encoded (needs numpy
and Pillow, both pulled in automatically). See Known limitations below for what this preview
pipeline does not do (debayering, precise processing).
Not every property needs a dedicated entity to be usable. Two services cover anything the automatic mapping does not (yet) turn into a nice entity:
# Re-request property definitions (e.g. after a driver was reloaded)
service: indi_client.refresh
data:
config_entry_id: <config entry id>
device: "Telescope Simulator" # optional
# Send a raw new*Vector command
service: indi_client.set_property
data:
config_entry_id: <config entry id>
device: "Telescope Simulator"
property: "TELESCOPE_PARK"
type: "Switch"
values:
PARK: "On"This integration is not yet in the default HACS store. Add it as a custom repository:
- HACS -> Integrations -> the
⋮menu (top right) -> Custom repositories. - URL:
https://github.com/jan-tdy/ha-indi-client, category: Integration. - Install INDI Client, then restart Home Assistant.
Copy custom_components/indi_client into your Home Assistant config/custom_components/
directory, then restart Home Assistant.
Everything is configured through the UI - no YAML required.
- Settings -> Devices & services -> Add integration -> INDI Client.
- Enter the host and port of your
indiserver(default port7624). - Home Assistant validates the connection and creates one entry per server. Entities appear
automatically as
indiserverannounces devices/properties - this can take a few seconds after setup, and again whenever a driver is (re)connected.
To change the host/port later, open the integration entry and use Configure (options flow).
Park the mount when it starts raining:
automation:
- alias: Park mount on rain
trigger:
- platform: state
entity_id: binary_sensor.rain_sensor
to: "on"
action:
- action: select.select_option
target:
entity_id: select.telescope_simulator_parking
data:
option: "Park"Cool the CCD down before an imaging session:
automation:
- alias: Cool CCD before imaging
trigger:
- platform: time
at: "21:30:00"
action:
- action: number.set_value
target:
entity_id: number.ccd_simulator_temperature
data:
value: -10Notify when a device reports an Alert state:
automation:
- alias: INDI device alert
trigger:
- platform: state
entity_id: sensor.telescope_simulator_last_message
condition:
- condition: template
value_template: "{{ 'Alert' in trigger.to_state.attributes.get('history', [''])[-1] }}"
action:
- action: notify.mobile_app
data:
message: "{{ trigger.to_state.state }}"- Camera previews are a quick look, not calibrated/processed data. Frames are decoded and
displayed with a simple percentile-clip stretch - there's no dark/flat calibration, no plate
solving. A driver's raw single-plane Bayer frame is not debayered by this integration and shows
as a grayscale mosaic pattern; a 3-plane RGB cube (
NAXIS=3, one plane each for R/G/B - what a driver sends once it has already debayered the frame itself) is stretched and shown in color. Use CCDciel/KStars for actual image processing; this is just a "is it working / roughly in focus" preview inside HA. Non-FITS, non-JPEG BLOB formats (e.g..xisf) aren't decoded. - Multi-element number vectors (e.g.
EQUATORIAL_EOD_COORDwith RA + DEC for a GOTO) are exposed as independentnumberentities. Setting one sends only that element; most drivers keep the other element's last known value, but this is driver-dependent. For coordinated multi-element commands, prefer theindi_client.set_propertyservice, or set both entities and use an automation with a short delay. - Number units are inferred heuristically from the element name (e.g.
*TEMPERATURE-> °C); INDI itself does not transmit explicit units. - Tested primarily against INDI's own driver simulators; real-hardware driver quirks may surface edge cases - please open an issue if you hit one.
The custom_components/indi_client/indi/ package (protocol parsing, wire formatting, the asyncio
client) is pure Python with no Home Assistant dependency, so it can be unit tested in isolation:
pip install -r requirements_test.txt
pytestHACS tracks GitHub Releases, not CHANGELOG.md directly - and releasing is fully automatic. Every
PR into main must bump version in custom_components/indi_client/manifest.json (CI-enforced by
.github/workflows/require-version-bump.yml; add a
CHANGELOG.md entry alongside it). Once merged,
.github/workflows/release.yml tags that version and publishes a
GitHub Release with auto-generated notes by itself - no manual git tag/git push needed. See
CLAUDE.md for the full flow.
MIT - permissive and compatible with HACS; contributions and forks are welcome.