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
84 changes: 80 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,97 @@
# unifi_respondd

This queries the API of a UniFi controller to get the current status of the Accesspoints and sends the information via the respondd protocol. Thus it can be picked up by `yanic` and other respondd queriers.
This tool queries controller APIs (UniFi, Omada, UISP, etc.) to get the current status of Access Points and sends the information via the respondd protocol. Thus it can be picked up by `yanic` and other respondd queriers.

## Overview

```mermaid
graph TD;
A{"*respondd_main*"} -->| | B("*unifi_client*")
A{"*respondd_main*"} -->| | B("*provider*")
A -->| | C("*respondd_client*")
B -->|"RestFul API"| D("unifi_controller")
B -->|"RestFul API"| D("unifi_controller / omada / uisp")
C -->|"Subscribe"| E("multicast")
C -->|"Send per interval / On multicast request"| F("unicast")
G{"yanic"} -->|"Request metrics"| E
F -->|"Receive"| G
```

## Config File:
## Multi-Provider Support

The tool now supports multiple controller providers in a single configuration. You can connect to multiple UniFi controllers, Omada controllers, or UISP systems simultaneously.

### Multi-Provider Config File (New Format):
```yaml
providers:
# UniFi Controller 1
- type: unifi
config:
controller_url: unifi1.lan
controller_port: 8443
username: ubnt
password: ubnt
ssid_regex: .*freifunk.*
offloader_mac:
SiteName: 00:00:00:00:00:00
nodelist: https://MAPURL/data/meshviewer.json
version: v5
ssl_verify: True
fallback_domain: "unifi_provider1"

# UniFi Controller 2
- type: unifi
config:
controller_url: unifi2.lan
controller_port: 8443
username: admin
password: admin123
ssid_regex: .*freifunk.*
offloader_mac:
SiteA: 11:11:11:11:11:11
nodelist: https://MAPURL/data/meshviewer.json
version: v5
ssl_verify: True
fallback_domain: "unifi_provider2"

# Future: TP-Link Omada support
# - type: omada
# config:
# controller_url: omada.lan
# ...

# Future: Ubiquiti UISP support
# - type: uisp
# config:
# controller_url: uisp.lan
# ...

# Respondd settings
multicast_enabled: false
multicast_address: ff05::2:1001
multicast_port: 1001
unicast_address: fe80::68ff:94ff:fe00:1504
unicast_port: 10001
interface: eth0
verbose: true
logging_config:
formatters:
standard:
format: '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s'
handlers:
console:
class: logging.StreamHandler
formatter: standard
root:
handlers:
- console
level: DEBUG
version: 1
```

See `unifi_respondd.multi-provider.yaml.example` for a complete example.

### Legacy Config File (Single UniFi Controller):
The legacy single-controller format is still supported for backward compatibility:

```yaml
controller_url: unifi.lan
controller_port: 8443
Expand Down
74 changes: 74 additions & 0 deletions unifi_respondd.multi-provider.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Multi-Provider Configuration Example
# This configuration format allows you to connect to multiple controllers/providers

providers:
# UniFi Controller 1
- type: unifi
config:
controller_url: unifi1.lan
controller_port: 8443
username: ubnt
password: ubnt
ssid_regex: .*freifunk.*
offloader_mac:
SiteName: 00:00:00:00:00:00
SiteName2: 00:00:00:00:00:00
nodelist: https://MAPURL/data/meshviewer.json
version: v5
ssl_verify: True
fallback_domain: "unifi_provider1"

# UniFi Controller 2 (optional - example of multiple UniFi controllers)
- type: unifi
config:
controller_url: unifi2.lan
controller_port: 8443
username: admin
password: admin123
ssid_regex: .*freifunk.*
offloader_mac:
SiteA: 11:11:11:11:11:11
nodelist: https://MAPURL/data/meshviewer.json
version: v5
ssl_verify: True
fallback_domain: "unifi_provider2"

# TP-Link Omada (future implementation)
# - type: omada
# config:
# controller_url: omada.lan
# controller_port: 8043
# username: admin
# password: password
# ssid_regex: .*freifunk.*
# nodelist: https://MAPURL/data/meshviewer.json

# Ubiquiti UISP (future implementation)
# - type: uisp
# config:
# controller_url: uisp.lan
# api_token: your_api_token_here
# ssid_regex: .*freifunk.*
# nodelist: https://MAPURL/data/meshviewer.json

# Respondd settings (same for all providers)
multicast_enabled: false
multicast_address: ff05::2:1001
multicast_port: 1001
unicast_address: fe80::68ff:94ff:fe00:1504
unicast_port: 10001
interface: eth0
verbose: true
logging_config:
formatters:
standard:
format: '%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s'
handlers:
console:
class: logging.StreamHandler
formatter: standard
root:
handlers:
- console
level: DEBUG
version: 1
117 changes: 91 additions & 26 deletions unifi_respondd/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,44 @@ class ConfigFileNotFoundError(Error):
"""File could not be found on disk."""


@dataclasses.dataclass
class ProviderConfig:
"""Configuration for a single provider.
Attributes:
type: The type of provider (e.g., 'unifi', 'omada', 'uisp').
config: Provider-specific configuration dictionary.
"""

type: str
config: Dict[str, Any]


@dataclasses.dataclass
class Config:
"""A representation of the configuration file.
Attributes:
controller_url: The unifi controller URL.
controller_port: The unifi Controller port.
username: The username for unifi controller.
password: The password for unifi controller.
providers: List of provider configurations (new format).
multicast_address: The multicast address for respondd.
multicast_port: The multicast port for respondd.
unicast_address: The unicast address for respondd.
unicast_port: The unicast port for respondd.
interface: The network interface to use.
verbose: Enable verbose logging.
multicast_enabled: Enable multicast support.

# Legacy fields for backward compatibility
controller_url: The unifi controller URL (deprecated).
controller_port: The unifi Controller port (deprecated).
username: The username for unifi controller (deprecated).
password: The password for unifi controller (deprecated).
ssid_regex: SSID regex pattern (deprecated).
offloader_mac: Offloader MAC addresses (deprecated).
nodelist: Nodelist URL (deprecated).
fallback_domain: Fallback domain (deprecated).
version: UniFi version (deprecated).
ssl_verify: SSL verification (deprecated).
"""

controller_url: str
controller_port: int
username: str
password: str
ssid_regex: str
offloader_mac: Dict[str, str]
nodelist: str
fallback_domain: str

multicast_address: str
multicast_port: int
unicast_address: str
Expand All @@ -45,36 +64,79 @@ class Config:
verbose: bool = False
multicast_enabled: bool = True

version: str = "v5"
ssl_verify: bool = True
# New multi-provider support
providers: Optional[List[ProviderConfig]] = None

# Legacy single-controller fields (for backward compatibility)
controller_url: Optional[str] = None
controller_port: Optional[int] = None
username: Optional[str] = None
password: Optional[str] = None
ssid_regex: Optional[str] = None
offloader_mac: Optional[Dict[str, str]] = None
nodelist: Optional[str] = None
fallback_domain: Optional[str] = None
version: Optional[str] = None
ssl_verify: Optional[bool] = None

@classmethod
def from_dict(cls, cfg: Dict[str, str]) -> "Config":
def from_dict(cls, cfg: Dict[str, Any]) -> "Config":
"""Creates a Config object from a configuration file.

Supports both legacy format (single UniFi controller) and new format (multiple providers).

Arguments:
cfg: The configuration file as a dict.
Returns:
A Config object.
"""
# Check if this is the new multi-provider format
providers = None
if "providers" in cfg:
providers = [
ProviderConfig(type=p["type"], config=p["config"])
for p in cfg["providers"]
]

# Handle legacy format - if no providers but has controller_url, create a provider
if providers is None and "controller_url" in cfg:
# Legacy format detected - create a single UniFi provider from root config
provider_config = {
"controller_url": cfg["controller_url"],
"controller_port": cfg["controller_port"],
"username": cfg["username"],
"password": cfg["password"],
"ssid_regex": cfg["ssid_regex"],
"offloader_mac": cfg["offloader_mac"],
"nodelist": cfg["nodelist"],
"fallback_domain": cfg.get(
"fallback_domain", "unifi_respondd_fallback"
),
"version": cfg.get("version", "v5"),
"ssl_verify": cfg.get("ssl_verify", True),
}
providers = [ProviderConfig(type="unifi", config=provider_config)]

return cls(
controller_url=cfg["controller_url"],
controller_port=cfg["controller_port"],
username=cfg["username"],
password=cfg["password"],
ssid_regex=cfg["ssid_regex"],
offloader_mac=cfg["offloader_mac"],
nodelist=cfg["nodelist"],
fallback_domain=cfg.get("fallback_domain", "unifi_respondd_fallback"),
version=cfg["version"],
ssl_verify=cfg["ssl_verify"],
providers=providers,
multicast_enabled=cfg["multicast_enabled"],
multicast_address=cfg["multicast_address"],
multicast_port=cfg["multicast_port"],
unicast_address=cfg["unicast_address"],
unicast_port=cfg["unicast_port"],
interface=cfg["interface"],
verbose=cfg["verbose"],
# Legacy fields (optional, only populated in legacy format)
controller_url=cfg.get("controller_url"),
controller_port=cfg.get("controller_port"),
username=cfg.get("username"),
password=cfg.get("password"),
ssid_regex=cfg.get("ssid_regex"),
offloader_mac=cfg.get("offloader_mac"),
nodelist=cfg.get("nodelist"),
fallback_domain=cfg.get("fallback_domain", "unifi_respondd_fallback"),
version=cfg.get("version", "v5"),
ssl_verify=cfg.get("ssl_verify", True),
)


Expand Down Expand Up @@ -105,6 +167,9 @@ def load_config() -> Dict[str, str]:
return config
except (KeyError, TypeError) as e:
print("Failed to lint file: %s", e)
print(
"Make sure your config has either 'providers' list or legacy UniFi controller fields"
)
sys.exit(2)


Expand Down
Loading
Loading