diff --git a/.gitignore b/.gitignore index e8e8443..e14ee7b 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ Thumbs.db # Poetry poetry.lock + +apps/homekit/homekit.state + diff --git a/apps/homekit/accessories/__init__.py b/apps/homekit/accessories/__init__.py new file mode 100644 index 0000000..a72fca6 --- /dev/null +++ b/apps/homekit/accessories/__init__.py @@ -0,0 +1,5 @@ +""" +HomeKit Accessories Package + +This package contains HomeKit accessory implementations for different device types. +""" diff --git a/apps/homekit/accessories/contact.py b/apps/homekit/accessories/contact.py new file mode 100644 index 0000000..bb2ce0a --- /dev/null +++ b/apps/homekit/accessories/contact.py @@ -0,0 +1,48 @@ +""" +Contact Sensor Accessory Implementation for HomeKit + +Implements contact sensor (window/door sensors): +- ContactSensorState (read-only): 0=Detected, 1=Not Detected +""" + +from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_SENSOR + + +class ContactAccessory(Accessory): + """Contact sensor for doors and windows.""" + + category = CATEGORY_SENSOR + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + """ + Initialize the contact sensor accessory. + + Args: + driver: HAP driver instance + device: Device object from DeviceRegistry + api_client: ApiClient for sending commands + display_name: Optional display name (defaults to device.friendly_name) + """ + name = display_name or device.friendly_name or device.name + super().__init__(driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + + # Add ContactSensor service + self.contact_service = self.add_preload_service('ContactSensor') + + # Get ContactSensorState characteristic + self.contact_state_char = self.contact_service.get_characteristic('ContactSensorState') + + # Initialize with "not detected" (closed) + self.contact_state_char.set_value(1) + + def update_state(self, state_payload): + """Update state from API event.""" + if "contact" in state_payload: + # API sends: "open" or "closed" + # HomeKit: 0=Contact Detected (closed), 1=Contact Not Detected (open) + is_open = state_payload["contact"] == "open" + homekit_state = 1 if is_open else 0 + self.contact_state_char.set_value(homekit_state) diff --git a/apps/homekit/accessories/light.py b/apps/homekit/accessories/light.py new file mode 100644 index 0000000..6331e30 --- /dev/null +++ b/apps/homekit/accessories/light.py @@ -0,0 +1,177 @@ +""" +Light Accessory Implementations for HomeKit + +Implements different light types: +- OnOffLightAccessory: Simple on/off light +- DimmableLightAccessory: Light with brightness control +- ColorLightAccessory: RGB light with full color control +""" + +from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_LIGHTBULB + + +class OnOffLightAccessory(Accessory): + """Simple On/Off Light without dimming or color.""" + + category = CATEGORY_LIGHTBULB + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + """ + Initialize the light accessory. + + Args: + driver: HAP driver instance + device: Device object from DeviceRegistry + api_client: ApiClient for sending commands + display_name: Optional display name (defaults to device.friendly_name) + """ + name = display_name or device.friendly_name or device.name + super().__init__(driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + + # Add Lightbulb service with On characteristic + self.lightbulb_service = self.add_preload_service('Lightbulb') + + # Get the On characteristic and set callback + self.on_char = self.lightbulb_service.get_characteristic('On') + self.on_char.setter_callback = self.set_on + + def set_on(self, value): + """Called when HomeKit wants to turn light on/off.""" + power_state = "on" if value else "off" + payload = { + "type": "light", + "payload": {"power": power_state} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def update_state(self, state_payload): + """Update state from API event.""" + if "power" in state_payload: + is_on = state_payload["power"] == "on" + self.on_char.set_value(is_on) + + +class DimmableLightAccessory(OnOffLightAccessory): + """Dimmable Light with brightness control.""" + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + # Don't call super().__init__() yet - we need to set up service first + name = display_name or device.friendly_name or device.name + Accessory.__init__(self, driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + self.category = CATEGORY_LIGHTBULB + + # Create Lightbulb service with all characteristics at once + from pyhap.loader import Loader + loader = Loader() + + # Create the service + lightbulb_service = loader.get_service('Lightbulb') + + # Add On characteristic + on_char = lightbulb_service.get_characteristic('On') + on_char.setter_callback = self.set_on + self.on_char = on_char + + # Add Brightness characteristic + brightness_char = loader.get_char('Brightness') + brightness_char.set_value(0) + brightness_char.setter_callback = self.set_brightness + lightbulb_service.add_characteristic(brightness_char) + self.brightness_char = brightness_char + + # Now add the complete service to the accessory + self.add_service(lightbulb_service) + self.lightbulb_service = lightbulb_service + + def set_brightness(self, value): + """Called when HomeKit wants to change brightness.""" + payload = { + "type": "light", + "payload": {"brightness": int(value)} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def update_state(self, state_payload): + """Update state from API event.""" + super().update_state(state_payload) + if "brightness" in state_payload: + self.brightness_char.set_value(state_payload["brightness"]) + + +class ColorLightAccessory(DimmableLightAccessory): + """RGB Light with full color control.""" + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + # Don't call super().__init__() - build everything from scratch + name = display_name or device.friendly_name or device.name + Accessory.__init__(self, driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + self.category = CATEGORY_LIGHTBULB + + # Create Lightbulb service with all characteristics at once + from pyhap.loader import Loader + loader = Loader() + + # Create the service + lightbulb_service = loader.get_service('Lightbulb') + + # Add On characteristic + on_char = lightbulb_service.get_characteristic('On') + on_char.setter_callback = self.set_on + self.on_char = on_char + + # Add Brightness characteristic + brightness_char = loader.get_char('Brightness') + brightness_char.set_value(0) + brightness_char.setter_callback = self.set_brightness + lightbulb_service.add_characteristic(brightness_char) + self.brightness_char = brightness_char + + # Add Hue characteristic + hue_char = loader.get_char('Hue') + hue_char.set_value(0) + hue_char.setter_callback = self.set_hue + lightbulb_service.add_characteristic(hue_char) + self.hue_char = hue_char + + # Add Saturation characteristic + saturation_char = loader.get_char('Saturation') + saturation_char.set_value(0) + saturation_char.setter_callback = self.set_saturation + lightbulb_service.add_characteristic(saturation_char) + self.saturation_char = saturation_char + + # Now add the complete service to the accessory + self.add_service(lightbulb_service) + self.lightbulb_service = lightbulb_service + + def set_hue(self, value): + """Called when HomeKit wants to change hue.""" + payload = { + "type": "light", + "payload": {"hue": int(value)} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def set_saturation(self, value): + """Called when HomeKit wants to change saturation.""" + payload = { + "type": "light", + "payload": {"sat": int(value)} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def update_state(self, state_payload): + """Update state from API event.""" + super().update_state(state_payload) + if "hue" in state_payload: + self.hue_char.set_value(state_payload["hue"]) + if "sat" in state_payload: + self.saturation_char.set_value(state_payload["sat"]) + diff --git a/apps/homekit/accessories/outlet.py b/apps/homekit/accessories/outlet.py new file mode 100644 index 0000000..82ae994 --- /dev/null +++ b/apps/homekit/accessories/outlet.py @@ -0,0 +1,57 @@ +""" +Outlet/Relay Accessory Implementation for HomeKit + +Implements simple relay/outlet (on/off switch): +- On (read/write) +- OutletInUse (always true) +""" + +from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_OUTLET + + +class OutletAccessory(Accessory): + """Relay/Outlet for simple on/off control.""" + + category = CATEGORY_OUTLET + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + """ + Initialize the outlet accessory. + + Args: + driver: HAP driver instance + device: Device object from DeviceRegistry + api_client: ApiClient for sending commands + display_name: Optional display name (defaults to device.friendly_name) + """ + name = display_name or device.friendly_name or device.name + super().__init__(driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + + # Add Outlet service + self.outlet_service = self.add_preload_service('Outlet') + + # Get On characteristic and set callback + self.on_char = self.outlet_service.get_characteristic('On') + self.on_char.setter_callback = self.set_on + + # OutletInUse is always true (relay is always functional) + self.in_use_char = self.outlet_service.get_characteristic('OutletInUse') + self.in_use_char.set_value(True) + + def set_on(self, value): + """Called when HomeKit wants to turn outlet on/off.""" + power_state = "on" if value else "off" + payload = { + "type": "relay", + "payload": {"power": power_state} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def update_state(self, state_payload): + """Update state from API event.""" + if "power" in state_payload: + is_on = state_payload["power"] == "on" + self.on_char.set_value(is_on) diff --git a/apps/homekit/accessories/sensor.py b/apps/homekit/accessories/sensor.py new file mode 100644 index 0000000..ce6b323 --- /dev/null +++ b/apps/homekit/accessories/sensor.py @@ -0,0 +1,46 @@ +""" +Temperature & Humidity Sensor Accessory Implementation for HomeKit + +Implements combined temperature and humidity sensor: +- CurrentTemperature (read-only) +- CurrentRelativeHumidity (read-only) +""" + +from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_SENSOR + + +class TempHumidityAccessory(Accessory): + """Combined temperature and humidity sensor.""" + + category = CATEGORY_SENSOR + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + """ + Initialize the temp/humidity sensor accessory. + + Args: + driver: HAP driver instance + device: Device object from DeviceRegistry + api_client: ApiClient for sending commands + display_name: Optional display name (defaults to device.friendly_name) + """ + name = display_name or device.friendly_name or device.name + super().__init__(driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + + # Add TemperatureSensor service + self.temp_service = self.add_preload_service('TemperatureSensor') + self.current_temp_char = self.temp_service.get_characteristic('CurrentTemperature') + + # Add HumiditySensor service + self.humidity_service = self.add_preload_service('HumiditySensor') + self.current_humidity_char = self.humidity_service.get_characteristic('CurrentRelativeHumidity') + + def update_state(self, state_payload): + """Update state from API event.""" + if "temperature" in state_payload: + self.current_temp_char.set_value(float(state_payload["temperature"])) + if "humidity" in state_payload: + self.current_humidity_char.set_value(float(state_payload["humidity"])) diff --git a/apps/homekit/accessories/thermostat.py b/apps/homekit/accessories/thermostat.py new file mode 100644 index 0000000..51e542c --- /dev/null +++ b/apps/homekit/accessories/thermostat.py @@ -0,0 +1,72 @@ +""" +Thermostat Accessory Implementation for HomeKit + +Implements thermostat control according to homekit_mapping.md: +- CurrentTemperature (read-only) +- TargetTemperature (read/write) +- CurrentHeatingCoolingState (fixed: 1 = heating) +- TargetHeatingCoolingState (fixed: 3 = auto) +""" + +from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_THERMOSTAT + + +class ThermostatAccessory(Accessory): + """Thermostat with temperature control.""" + + category = CATEGORY_THERMOSTAT + + def __init__(self, driver, device, api_client, display_name=None, *args, **kwargs): + """ + Initialize the thermostat accessory. + + Args: + driver: HAP driver instance + device: Device object from DeviceRegistry + api_client: ApiClient for sending commands + display_name: Optional display name (defaults to device.friendly_name) + """ + name = display_name or device.friendly_name or device.name + super().__init__(driver, name, *args, **kwargs) + self.device = device + self.api_client = api_client + + # Add Thermostat service + self.thermostat_service = self.add_preload_service('Thermostat') + + # Get characteristics + self.current_temp_char = self.thermostat_service.get_characteristic('CurrentTemperature') + self.target_temp_char = self.thermostat_service.get_characteristic('TargetTemperature') + self.current_heating_cooling_char = self.thermostat_service.get_characteristic('CurrentHeatingCoolingState') + self.target_heating_cooling_char = self.thermostat_service.get_characteristic('TargetHeatingCoolingState') + + # Set callback for target temperature + self.target_temp_char.setter_callback = self.set_target_temperature + + # Set fixed heating/cooling states (mode is always "auto") + # CurrentHeatingCoolingState: 0=Off, 1=Heat, 2=Cool + self.current_heating_cooling_char.set_value(1) # Always heating + + # TargetHeatingCoolingState: 0=Off, 1=Heat, 2=Cool, 3=Auto + self.target_heating_cooling_char.set_value(3) # Always auto + + # Set temperature range (5-30°C as per UI) + self.target_temp_char.properties['minValue'] = 5 + self.target_temp_char.properties['maxValue'] = 30 + self.target_temp_char.properties['minStep'] = 0.5 + + def set_target_temperature(self, value): + """Called when HomeKit wants to change target temperature.""" + payload = { + "type": "thermostat", + "payload": {"target": float(value)} + } + self.api_client.post_device_set(self.device.device_id, payload["type"], payload["payload"]) + + def update_state(self, state_payload): + """Update state from API event.""" + if "current" in state_payload: + self.current_temp_char.set_value(float(state_payload["current"])) + if "target" in state_payload: + self.target_temp_char.set_value(float(state_payload["target"])) diff --git a/apps/homekit/api_client.py b/apps/homekit/api_client.py new file mode 100644 index 0000000..d147a36 --- /dev/null +++ b/apps/homekit/api_client.py @@ -0,0 +1,161 @@ +""" +API Client for HomeKit Bridge + +Handles all HTTP communication with the REST API backend. +""" + +import logging +from typing import Dict, List, Iterator, Optional +import httpx +import json +import time + +logger = logging.getLogger(__name__) + + +class ApiClient: + """HTTP client for communicating with the home automation API.""" + + def __init__(self, base_url: str, token: Optional[str] = None, timeout: int = 5): + """ + Initialize API client. + + Args: + base_url: Base URL of the API (e.g., "http://192.168.1.100:8001") + token: Optional API token for authentication + timeout: Request timeout in seconds + """ + self.base_url = base_url.rstrip('/') + self.timeout = timeout + self.headers = {} + + if token: + self.headers['Authorization'] = f'Bearer {token}' + + def get_devices(self) -> List[Dict]: + """ + Get list of all devices. + + Returns: + List of device dictionaries + """ + try: + response = httpx.get( + f'{self.base_url}/devices', + headers=self.headers, + timeout=self.timeout + ) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get devices: {e}") + raise + + def get_layout(self) -> Dict: + """ + Get layout information (rooms and device assignments). + + Returns: + Layout dictionary with room structure + """ + try: + response = httpx.get( + f'{self.base_url}/layout', + headers=self.headers, + timeout=self.timeout + ) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get layout: {e}") + raise + + def get_device_state(self, device_id: str) -> Dict: + """ + Get current state of a specific device. + + Args: + device_id: Device identifier + + Returns: + Device state dictionary + """ + try: + response = httpx.get( + f'{self.base_url}/devices/{device_id}/state', + headers=self.headers, + timeout=self.timeout + ) + response.raise_for_status() + return response.json() + except Exception as e: + logger.error(f"Failed to get state for {device_id}: {e}") + raise + + def post_device_set(self, device_id: str, device_type: str, payload: Dict) -> None: + """ + Send command to a device. + + Args: + device_id: Device identifier + device_type: Device type (e.g., "light", "thermostat") + payload: Command payload (e.g., {"power": "on", "brightness": 75}) + """ + try: + data = { + "type": device_type, + "payload": payload + } + response = httpx.post( + f'{self.base_url}/devices/{device_id}/set', + headers=self.headers, + json=data, + timeout=self.timeout + ) + response.raise_for_status() + logger.debug(f"Set {device_id}: {payload}") + except Exception as e: + logger.error(f"Failed to set {device_id}: {e}") + raise + + def stream_realtime(self, reconnect_delay: int = 5) -> Iterator[Dict]: + """ + Stream real-time events from the API using Server-Sent Events (SSE). + + Automatically reconnects on connection loss. + + Args: + reconnect_delay: Seconds to wait before reconnecting + + Yields: + Event dictionaries: {"type": "state", "device_id": "...", "payload": {...}, "ts": ...} + """ + while True: + try: + logger.info("Connecting to realtime event stream...") + with httpx.stream( + 'GET', + f'{self.base_url}/realtime', + headers=self.headers, + timeout=None # No timeout for streaming + ) as response: + response.raise_for_status() + logger.info("Connected to realtime event stream") + + for line in response.iter_lines(): + if line.startswith('data: '): + data_str = line[6:] # Remove 'data: ' prefix + try: + event = json.loads(data_str) + yield event + except json.JSONDecodeError as e: + logger.warning(f"Failed to parse SSE event: {e}") + + except httpx.HTTPError as e: + logger.error(f"Realtime stream error: {e}") + logger.info(f"Reconnecting in {reconnect_delay} seconds...") + time.sleep(reconnect_delay) + except Exception as e: + logger.error(f"Unexpected error in realtime stream: {e}") + logger.info(f"Reconnecting in {reconnect_delay} seconds...") + time.sleep(reconnect_delay) diff --git a/apps/homekit/device_registry.py b/apps/homekit/device_registry.py new file mode 100644 index 0000000..d9402a4 --- /dev/null +++ b/apps/homekit/device_registry.py @@ -0,0 +1,138 @@ +""" +Device Registry for HomeKit Bridge + +Loads devices from API and joins with layout information. +""" + +import logging +from typing import Dict, List, Optional +from dataclasses import dataclass + +logger = logging.getLogger(__name__) + + +@dataclass +class Device: + """Represents a device with combined info from /devices and /layout.""" + + device_id: str + type: str # "light", "thermostat", "relay", "contact", "temp_humidity", "cover" + name: str # Short name from /devices + friendly_name: str # Display title from /layout (fallback to name) + room: Optional[str] # Room name from layout + features: Dict[str, bool] # Feature flags (e.g., {"power": true, "brightness": true}) + read_only: bool # True for sensors that don't accept commands + + +class DeviceRegistry: + """Registry of all devices loaded from the API.""" + + def __init__(self, devices: List[Device]): + """ + Initialize registry with devices. + + Args: + devices: List of Device objects + """ + self._devices = devices + self._by_id = {d.device_id: d for d in devices} + + @classmethod + def load_from_api(cls, api_client) -> 'DeviceRegistry': + """ + Load devices from API and join with layout information. + + Args: + api_client: ApiClient instance + + Returns: + DeviceRegistry with all devices + """ + # Get devices and layout + devices_data = api_client.get_devices() + layout_data = api_client.get_layout() + + # Build lookup: device_id -> (room_name, title) + layout_map = {} + if isinstance(layout_data, dict) and 'rooms' in layout_data: + rooms_list = layout_data['rooms'] + if isinstance(rooms_list, list): + for room in rooms_list: + if isinstance(room, dict): + room_name = room.get('name', 'Unknown') + devices_in_room = room.get('devices', []) + for device_info in devices_in_room: + if isinstance(device_info, dict): + device_id = device_info.get('device_id') + title = device_info.get('title', '') + if device_id: + layout_map[device_id] = (room_name, title) + + # Create Device objects + devices = [] + for dev_data in devices_data: + device_id = dev_data.get('device_id') + if not device_id: + logger.warning(f"Device without device_id: {dev_data}") + continue + + # Get layout info + room_name, title = layout_map.get(device_id, (None, '')) + + # Determine if read-only (sensors don't accept set commands) + device_type = dev_data.get('type', '') + read_only = device_type in ['contact', 'temp_humidity', 'motion', 'smoke'] + + device = Device( + device_id=device_id, + type=device_type, + name=dev_data.get('name', device_id), + friendly_name=title or dev_data.get('name', device_id), + room=room_name, + features=dev_data.get('features', {}), + read_only=read_only + ) + devices.append(device) + + logger.info(f"Loaded {len(devices)} devices from API") + return cls(devices) + + def get_all(self) -> List[Device]: + """Get all devices.""" + return self._devices.copy() + + def get_by_id(self, device_id: str) -> Optional[Device]: + """ + Get device by ID. + + Args: + device_id: Device identifier + + Returns: + Device or None if not found + """ + return self._by_id.get(device_id) + + def get_by_type(self, device_type: str) -> List[Device]: + """ + Get all devices of a specific type. + + Args: + device_type: Device type (e.g., "light", "thermostat") + + Returns: + List of matching devices + """ + return [d for d in self._devices if d.type == device_type] + + def get_by_room(self, room: str) -> List[Device]: + """ + Get all devices in a specific room. + + Args: + room: Room name + + Returns: + List of devices in the room + """ + return [d for d in self._devices if d.room == room] diff --git a/apps/homekit/main.py b/apps/homekit/main.py index 226a93e..adb791b 100644 --- a/apps/homekit/main.py +++ b/apps/homekit/main.py @@ -1,8 +1,34 @@ +""" +HomeKit Bridge Main Module + +Implementiert eine HAP-Python Bridge, die Geräte über die REST-API lädt +und über HomeKit verfügbar macht. + +Für detaillierte Implementierungsanweisungen, Tests und Deployment-Informationen +siehe README.md in diesem Verzeichnis. +""" + +import os +import logging +import signal +import sys +import threading +from typing import Optional + +from pyhap.accessory_driver import AccessoryDriver +from pyhap.accessory import Bridge + from .accessories.light import ( OnOffLightAccessory, DimmableLightAccessory, ColorLightAccessory, ) +from .accessories.thermostat import ThermostatAccessory +from .accessories.contact import ContactAccessory +from .accessories.sensor import TempHumidityAccessory +from .accessories.outlet import OutletAccessory +from .api_client import ApiClient +from .device_registry import DeviceRegistry # Configure logging logging.basicConfig( @@ -45,9 +71,14 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge: try: accessory = create_accessory_for_device(device, api_client, driver) if accessory: + # Set room information in the accessory (HomeKit will use this for suggestions) + if device.room: + # Store room info for potential future use + accessory._room_name = device.room + bridge.add_accessory(accessory) accessory_map[device.device_id] = accessory - logger.info(f"Added accessory: {device.friendly_name} ({device.type})") + logger.info(f"Added accessory: {device.friendly_name} ({device.type}) in room: {device.room or 'Unknown'}") else: logger.warning(f"No accessory mapping for device: {device.name} ({device.type})") except Exception as e: @@ -60,6 +91,22 @@ def build_bridge(driver: AccessoryDriver, api_client: ApiClient) -> Bridge: return bridge +def get_accessory_name(device) -> str: + """ + Build accessory name including room information. + + Args: + device: Device object from DeviceRegistry + + Returns: + Name string like "Device Name (Room)" or just "Device Name" if no room + """ + base_name = device.friendly_name or device.name + if device.room: + return f"{base_name} ({device.room})" + return base_name + + def create_accessory_for_device(device, api_client: ApiClient, driver: AccessoryDriver): """ Create appropriate HomeKit accessory based on device type and features. @@ -68,36 +115,38 @@ def create_accessory_for_device(device, api_client: ApiClient, driver: Accessory """ device_type = device.type features = device.features + display_name = get_accessory_name(device) # Light accessories if device_type == "light": if features.get("color_hsb"): - return ColorLightAccessory(driver, device, api_client) + return ColorLightAccessory(driver, device, api_client, display_name=display_name) elif features.get("brightness"): - return DimmableLightAccessory(driver, device, api_client) + return DimmableLightAccessory(driver, device, api_client, display_name=display_name) else: - return OnOffLightAccessory(driver, device, api_client) + return OnOffLightAccessory(driver, device, api_client, display_name=display_name) # Thermostat elif device_type == "thermostat": - return ThermostatAccessory(driver, device, api_client) + return ThermostatAccessory(driver, device, api_client, display_name=display_name) # Contact sensor elif device_type == "contact": - return ContactAccessory(driver, device, api_client) + return ContactAccessory(driver, device, api_client, display_name=display_name) # Temperature/Humidity sensor - elif device_type == "temp_humidity": - return TempHumidityAccessory(driver, device, api_client) + elif device_type == "temp_humidity_sensor": + return TempHumidityAccessory(driver, device, api_client, display_name=display_name) - # Outlet/Switch - elif device_type == "outlet": - return OutletAccessory(driver, device, api_client) + # Relay/Outlet + elif device_type == "relay": + return OutletAccessory(driver, device, api_client, display_name=display_name) # Cover/Blinds (optional) elif device_type == "cover": # TODO: Implement CoverAccessory based on homekit_mapping.md - return CoverAccessory(driver, device, api_client) + logger.warning(f"Cover accessory not yet implemented for {device.name}") + return None # TODO: Add more device types as needed (lock, motion, etc.) diff --git a/apps/homekit/start_bridge.sh b/apps/homekit/start_bridge.sh index 291b556..2d206df 100755 --- a/apps/homekit/start_bridge.sh +++ b/apps/homekit/start_bridge.sh @@ -5,12 +5,17 @@ set -e # Exit on error -# Change to script directory +# Determine script directory (apps/homekit) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" + +# Navigate to workspace root (two levels up from apps/homekit) +WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +cd "$WORKSPACE_ROOT" echo "🏠 HomeKit Bridge Startup" echo "=========================" +echo " Working dir: $WORKSPACE_ROOT" +echo "" # Virtual environment path VENV_DIR="$SCRIPT_DIR/venv" @@ -18,7 +23,19 @@ VENV_DIR="$SCRIPT_DIR/venv" # Check if virtual environment exists if [ ! -d "$VENV_DIR" ]; then echo "📦 Virtual environment not found. Creating..." - python3 -m venv "$VENV_DIR" + # Try to use Python 3.12 or 3.13 (3.14 has compatibility issues with HAP-Python) + if command -v python3.13 &> /dev/null; then + PYTHON_CMD=python3.13 + elif command -v python3.12 &> /dev/null; then + PYTHON_CMD=python3.12 + elif command -v python3.11 &> /dev/null; then + PYTHON_CMD=python3.11 + else + PYTHON_CMD=python3 + echo "⚠️ Warning: Using default python3. HAP-Python may not work with Python 3.14+" + fi + echo " Using: $PYTHON_CMD" + $PYTHON_CMD -m venv "$VENV_DIR" echo "✅ Virtual environment created at $VENV_DIR" fi @@ -29,7 +46,7 @@ source "$VENV_DIR/bin/activate" # Install/update dependencies echo "📥 Installing dependencies from requirements.txt..." pip install --upgrade pip -q -pip install -r requirements.txt -q +pip install -r "$SCRIPT_DIR/requirements.txt" -q echo "✅ Dependencies installed" # Set environment variables (with defaults) @@ -55,5 +72,5 @@ echo "🚀 Starting HomeKit Bridge..." echo " (Press Ctrl+C to stop)" echo "" -# Run the bridge using Python module syntax +# Run the bridge from workspace root with correct module path python -m apps.homekit.main