Radiale is a home automation project.
It leverages the power of Clojure for its core logic, Python for specific integrations, and Babashka pods to extend its capabilities.
The project is organized into the following main directories:
radiale/
: This directory contains Python modules for specific integrations.__init__.py
: Initializes the Python package.chromecast.py
: Module for interacting with Chromecast devices.deconz.py
: Module for interacting with deCONZ (Zigbee gateway).esphome.py
: Module for interacting with ESPHome devices.mdns.py
: Module for mDNS discovery.mqtt.py
: Module for MQTT communication.pod.py
: Core module for Babashka pod interaction.schedule.py
: Module for scheduling tasks.
src/
: This directory contains the Clojure source code for the core logic of Radiale.radiale/
: This sub-directory contains the Clojure namespaces.chromecast.clj
: Clojure wrapper for Chromecast functionality.core.clj
: Main entry point and core logic of the Radiale application.deconz.clj
: Clojure wrapper for deCONZ functionality.esp.clj
: Clojure wrapper for ESPHome functionality.influx.clj
: Module for interacting with InfluxDB (time-series database).schedule.clj
: Clojure wrapper for task scheduling.state.clj
: Manages the state of the application.watch.clj
: Module for watching changes (e.g., file system, network).
Key files at the root level:
bb.edn
: Babashka project configuration file.deps.edn
: Clojure dependencies configuration file.pod-xlfe-radiale.py
: Script for the Radiale Babashka pod.setup.py
: Python package setup script.requirements.txt
: Python dependencies.start.sh
: Script to start the Radiale application.Dockerfile-dev
: Docker configuration for the development environment.
Radiale's architecture revolves around a few key concepts:
-
Clojure-Python Interaction (Babashka Pods):
- The core logic resides in Clojure (
src/radiale/core.clj
), while device-specific integrations and other functionalities are often implemented in Python (radiale/
directory). - Babashka pods serve as the bridge between these two languages. The
pod-xlfe-radiale.py
script defines a Babashka pod that exposes Python functions to the Clojure environment. - Communication between Clojure and the Python pod is handled using bencode for message serialization. The
radiale/pod.py
module in Python manages the pod's lifecycle, message decoding/encoding, and invoking the appropriate Python functions based on requests from Clojure. - Clojure functions (e.g., in
src/radiale/esp.clj
,src/radiale/deconz.clj
) wrap these pod invocations, providing a seamless interface for the core application logic.
- The core logic resides in Clojure (
-
State Management (
src/radiale/state.clj
):- Radiale maintains an application state as a Clojure atom (
state*
insrc/radiale/core.clj
). - The
src/radiale/state.clj
namespace provides mechanisms to watch for changes in this state. - When parts of the state are updated (e.g., a device status changes), registered watch functions are triggered.
- This allows different parts of the application to react to state changes. For example, a change in a sensor's state might trigger an automation rule.
- The
unpack
function instate.clj
helps in dissecting state changes to identify the specific domain, device, and property that changed, along with its previous and current values.
- Radiale maintains an application state as a Clojure atom (
-
Message Passing and Event Loop (
src/radiale/core.clj
):- Radiale uses a central event loop implemented with
core.async
channels. A mainsend-chan
is used to pass messages or events throughout the application. - The
run
function incore.clj
initializes this channel and enters a loop, continuously taking messages fromsend-chan
. - Messages are typically maps that can specify a function to execute (
::fn
) or a subsequent action/transformation (::then
). - The
try-fn
function is responsible for processing these messages. It can:- Invoke a function directly if
::fn
is present. - Process a
::then
clause, which can be another function, a map to merge, or a sequence of actions to perform. - Utilize
watch/match-message
to trigger actions based on message patterns.
- Invoke a function directly if
- This message-passing architecture allows for decoupled components and asynchronous operations.
- Radiale uses a central event loop implemented with
-
Scheduling (
src/radiale/schedule.clj
andradiale/schedule.py
):- Radiale supports task scheduling based on time, solar events (sunrise/sunset), and cron-like expressions.
- The Clojure side (
src/radiale/schedule.clj
) uses theovertone/at-at
library for managing scheduled jobs. - It provides functions like
crontab
,solar
,after
(run once after a delay), andevery
(run periodically). - These functions typically interact with the Python pod (via
radiale/millis-solar
andradiale/millis-crontab
ops defined inradiale/pod.py
) to calculate the milliseconds until the next scheduled event. - The Python module
radiale/schedule.py
contains the logic for these calculations (e.g.,ms_until_solar
,ms_until_crontab
). - Scheduled tasks result in messages being put onto the main
send-chan
for processing by the core event loop. - The
::at-most-once
option in scheduling helps prevent duplicate job scheduling by using a unique identifier.
Radiale integrates with various devices and services:
-
Chromecast:
- Utilizes mDNS for discovery (
_googlecast._tcp.local.
). - The
radiale.chromecast
Python module, usingdmcast
, connects to Chromecast devices and notifies of state changes. src/radiale/chromecast.clj
provides functions to discover and store Chromecast properties in the application state.- Allows sending commands to Chromecast devices (though specific commands are not detailed in
chromecast.py
'scommand
method, it suggests generic command forwarding).
- Utilizes mDNS for discovery (
-
deCONZ (Zigbee Gateway):
- Connects to a deCONZ gateway via its REST API and WebSocket for real-time event listening.
radiale.deconz
Python module handles API requests (e.g., getting configuration, putting device states) and listens for WebSocket events.src/radiale/deconz.clj
manages deCONZ device discovery, stores their configuration and state, and provides aput
function to send commands (e.g., turn lights on/off, change brightness).- State changes from deCONZ (e.g., sensor updates, light status) are processed and reflected in the application state.
-
ESPHome:
- Integrates with ESPHome devices using the
aioesphomeapi
library. - Discovery is done via mDNS (
_esphomelib._tcp.local.
). radiale.esphome
Python module establishes connections, handles device state subscriptions, lists available entities/services, and executes commands (switch, light, user-defined services). It also handles reconnection logic.src/radiale/esp.clj
processes discovery results, stores ESPHome device services and their states, and provides functions to send commands (switch
,light
,service
) and update Home Assistant states on the ESPHome device.
- Integrates with ESPHome devices using the
-
MQTT:
- Provides a generic MQTT client using
asyncio-mqtt
. radiale.mqtt
Python module can connect to an MQTT broker and subscribe to all topics (#
).- Received MQTT messages (topic and payload) are forwarded to the Clojure core for processing.
- The Clojure side (
src/radiale/core.clj
registerslisten-mqtt
) can then react to these messages, allowing integration with any device or service that communicates over MQTT.
- Provides a generic MQTT client using
- Java: Required for running Clojure. Version 19 is used in the Docker setup, but other recent LTS versions should work.
- Python: Required for the Babashka pod and various integrations. Version 3.9 is specified.
- Clojure CLI Tools: Needed to manage Clojure dependencies and run the application.
- Babashka (optional but recommended for pod development/testing): For working with Babashka pods directly.
-
Clojure Dependencies: Defined in
deps.edn
. Key dependencies include:org.clojure/clojure
org.clojure/core.async
com.taoensso/timbre
(for logging)net.xlfe/at-at
(for scheduling)babashka/babashka.pods
(for Python pod interaction) These will be fetched automatically by the Clojure CLI.
-
Python Dependencies: Listed in
setup.py
underinstall_requires
. Key dependencies include:protobuf
aioesphomeapi
websockets
aiohttp
dmcast
(for Chromecast)zeroconf
(for mDNS)bcoding
(for pod communication)asyncio-mqtt
astral
(for solar scheduling) You can install these using pip, typically viapip install -e .
orpython setup.py develop
from the project root.
- A
Dockerfile-dev
is provided, which sets up an environment with Java, Python, Clojure tools, and installs all dependencies. This can be a convenient way to get started or ensure a consistent environment. - Build the Docker image and run it, referring to the
start.sh
script as the entry point.
The main application configuration is typically loaded from a Clojure (EDN) file. The start.sh
script executes clojure -i config/setup.clj
. This suggests that config/setup.clj
is the entry point for loading your specific setup, which would in turn likely load an EDN configuration file.
- EDN Configuration File: While a specific example like
config.edn.example
is not present in the root, you will need to create a configuration file (e.g.,config/my_config.edn
). - This file will define:
- Credentials and connection details for services like deCONZ (API key, host), MQTT (broker address, credentials).
- Definitions of your devices and how they map to Radiale's internal identifiers.
- Automation rules, schedules, and event handlers.
- Refer to the
src/radiale/core.clj
run
function, which expects a sequence of configuration maps. Each map defines an initial setup or listener. - Examine the various
discover
and command functions insrc/radiale/deconz.clj
,src/radiale/esp.clj
, etc., to understand the parameters they require (e.g.,::api-key
,::host
for deCONZ).
- Ensure all dependencies (Clojure and Python) are installed.
- Create your configuration file(s) as described in the "Configuration" section. Modify
config/setup.clj
if necessary to point to your main configuration EDN file. - Execute the
start.sh
script:Alternatively, if running without the script, you would use the Clojure CLI. Since./start.sh
start.sh
usesclojure -i config/setup.clj
(which loads and executes the script), you would replicate that ifconfig/setup.clj
is your main entry point for running the application:Ensure the Python pod script (clojure -i config/setup.clj
pod-xlfe-radiale.py
) is executable and its path is correctly referenced by the Clojure code.start.sh
is generally the recommended method.
This is a hypothetical example to illustrate how you might structure your configuration. The actual keywords and structure will depend on the implementation in core.clj
and related modules. (Note: Clojure keywords like ::ident
often use namespaces, e.g., :some.namespace/livingroom_light
or ::alias/livingroom_light
, for clarity and to avoid collisions, depending on how they are defined and used in the code.)
[
;; Initialize deCONZ connection and discovery
{:fn radiale.deconz/discover
::host "deconz.local"
::api-key "YOUR_DECONZ_API_KEY"
::service-type-namespaces {:lights :radiale.deconz/light ; Example of namespaced keyword
:sensors :radiale.deconz/sensor}}
;; Initialize ESPHome discovery
{:fn radiale.esp/discover}
;; Initialize MQTT listener
{:fn pod.xlfe.radiale/listen-mqtt ; Direct call to a pod function
:host "mqtt.local"
:username "mqtt_user"
:password "mqtt_pass"}
;; Define a scheduled task: Turn on 'livingroom.light_main' at sunset
{:fn radiale.schedule/solar
::rc/desc "Sunset lights on"
::params {:event "sunset" :lat 51.50 :lon -0.12 :tz "Europe/London"} ; Example coordinates
::at-most-once :sunset_livingroom_light_on ; Unique ID for the schedule
::then {:fn radiale.deconz/put
::ident :radiale.deconz/livingroom_light_main ; Assuming 'livingroom_light_main' is unique within this namespace
::state {:on true :bri 254}}}
;; Automation: When a specific MQTT message is received, toggle an ESPHome switch
{:fn radiale.watch/add-watch
:path [:mqtt/messages "my/custom/topic/toggle"] ; Path to watch in the state
::then {:fn radiale.esp/switch
::ident :radiale.esphome/my_esp_switch ; Example of a more specific namespaced ident
::state :toggle}} ; Assuming :toggle is a valid state, otherwise true/false
;; Simple periodic task: Log a message every 5 minutes
{:fn radiale.schedule/every
::rc/desc "Periodic log"
::seconds (* 5 60)
::at-most-once :periodic_log_message
::then {:fn (fn [_ _ _ _] (taoensso.timbre/info "5 minutes elapsed"))}} ; Inline function
]
Note: This example is illustrative. You'll need to adapt it based on the actual data structures and functions available in Radiale's Clojure modules. The ::fn
key points to a Clojure function to be called, and other keys provide parameters for that function. The ::then
key specifies what to do after the initial function completes or an event occurs.
Contributions are welcome! Whether it's reporting a bug, suggesting a new feature, or submitting a pull request, your input is valuable. Please feel free to open an issue or a PR on the project's repository.