mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-17 04:58:19 -04:00
created python heater to contron the hln heater
This commit is contained in:
1
python/heater/__init__.py
Normal file
1
python/heater/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tuya heater control service."""
|
||||
68
python/heater/controller.py
Normal file
68
python/heater/controller.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""TinyTuya device controller for heater."""
|
||||
|
||||
import logging
|
||||
|
||||
import tinytuya
|
||||
|
||||
from python.heater.models import ActionResult, DeviceConfig, HeaterStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DPS mapping for heater
|
||||
DPS_POWER = "1" # bool: on/off
|
||||
DPS_SETPOINT = "101" # int: target temp (read-only)
|
||||
DPS_STATE = "102" # str: "Stop", "Heat", etc.
|
||||
DPS_UNKNOWN = "104" # int: unknown
|
||||
DPS_ERROR = "108" # int: last error code
|
||||
|
||||
|
||||
class HeaterController:
|
||||
"""Controls a Tuya heater device via local network."""
|
||||
|
||||
def __init__(self, config: DeviceConfig) -> None:
|
||||
self.device = tinytuya.Device(config.device_id, config.ip, config.local_key)
|
||||
self.device.set_version(config.version)
|
||||
self.device.set_socketTimeout(0.5)
|
||||
self.device.set_socketRetryLimit(1)
|
||||
|
||||
def status(self) -> HeaterStatus:
|
||||
"""Get current heater status."""
|
||||
data = self.device.status()
|
||||
|
||||
if "Error" in data:
|
||||
logger.error("Device error: %s", data)
|
||||
return HeaterStatus(power=False, raw_dps={"error": data["Error"]})
|
||||
|
||||
dps = data.get("dps", {})
|
||||
return HeaterStatus(
|
||||
power=bool(dps.get(DPS_POWER, False)),
|
||||
setpoint=dps.get(DPS_SETPOINT),
|
||||
state=dps.get(DPS_STATE),
|
||||
error_code=dps.get(DPS_ERROR),
|
||||
raw_dps=dps,
|
||||
)
|
||||
|
||||
def turn_on(self) -> ActionResult:
|
||||
"""Turn heater on."""
|
||||
try:
|
||||
self.device.set_value(DPS_POWER, True)
|
||||
return ActionResult(success=True, action="on", power=True)
|
||||
except Exception as error:
|
||||
logger.exception("Failed to turn on")
|
||||
return ActionResult(success=False, action="on", error=str(error))
|
||||
|
||||
def turn_off(self) -> ActionResult:
|
||||
"""Turn heater off."""
|
||||
try:
|
||||
self.device.set_value(DPS_POWER, False)
|
||||
return ActionResult(success=True, action="off", power=False)
|
||||
except Exception as error:
|
||||
logger.exception("Failed to turn off")
|
||||
return ActionResult(success=False, action="off", error=str(error))
|
||||
|
||||
def toggle(self) -> ActionResult:
|
||||
"""Toggle heater power state."""
|
||||
status = self.status()
|
||||
if status.power:
|
||||
return self.turn_off()
|
||||
return self.turn_on()
|
||||
85
python/heater/main.py
Normal file
85
python/heater/main.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""FastAPI heater control service."""
|
||||
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
|
||||
from python.common import configure_logger
|
||||
from python.heater.controller import HeaterController
|
||||
from python.heater.models import ActionResult, DeviceConfig, HeaterStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_app(config: DeviceConfig) -> FastAPI:
|
||||
"""Create FastAPI application."""
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
||||
app.state.controller = HeaterController(config)
|
||||
yield
|
||||
|
||||
app = FastAPI(
|
||||
title="Heater Control API",
|
||||
description="Fast local control for Tuya heater",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@app.get("/status")
|
||||
def get_status() -> HeaterStatus:
|
||||
return app.state.controller.status()
|
||||
|
||||
@app.post("/on")
|
||||
def heater_on() -> ActionResult:
|
||||
result = app.state.controller.turn_on()
|
||||
if not result.success:
|
||||
raise HTTPException(status_code=500, detail=result.error)
|
||||
return result
|
||||
|
||||
@app.post("/off")
|
||||
def heater_off() -> ActionResult:
|
||||
result = app.state.controller.turn_off()
|
||||
if not result.success:
|
||||
raise HTTPException(status_code=500, detail=result.error)
|
||||
return result
|
||||
|
||||
@app.post("/toggle")
|
||||
def heater_toggle() -> ActionResult:
|
||||
result = app.state.controller.toggle()
|
||||
if not result.success:
|
||||
raise HTTPException(status_code=500, detail=result.error)
|
||||
return result
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def serve(
|
||||
host: Annotated[str, typer.Option("--host", "-h", help="Host to bind to")],
|
||||
port: Annotated[int, typer.Option("--port", "-p", help="Port to bind to")] = 8124,
|
||||
log_level: Annotated[str, typer.Option("--log-level", "-l", help="Log level")] = "INFO",
|
||||
device_id: Annotated[str | None, typer.Option("--device-id", envvar="TUYA_DEVICE_ID")] = None,
|
||||
device_ip: Annotated[str | None, typer.Option("--device-ip", envvar="TUYA_DEVICE_IP")] = None,
|
||||
local_key: Annotated[str | None, typer.Option("--local-key", envvar="TUYA_LOCAL_KEY")] = None,
|
||||
) -> None:
|
||||
"""Start the heater control API server."""
|
||||
configure_logger(log_level)
|
||||
|
||||
logger.info("Starting heater control API server")
|
||||
|
||||
if not device_id or not device_ip or not local_key:
|
||||
error = "Must provide device ID, IP, and local key"
|
||||
raise typer.Exit(error)
|
||||
|
||||
config = DeviceConfig(device_id=device_id, ip=device_ip, local_key=local_key)
|
||||
|
||||
app = create_app(config)
|
||||
uvicorn.run(app, host=host, port=port)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
typer.run(serve)
|
||||
31
python/heater/models.py
Normal file
31
python/heater/models.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""Pydantic models for heater API."""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DeviceConfig(BaseModel):
|
||||
"""Tuya device configuration."""
|
||||
|
||||
device_id: str
|
||||
ip: str
|
||||
local_key: str
|
||||
version: float = 3.5
|
||||
|
||||
|
||||
class HeaterStatus(BaseModel):
|
||||
"""Current heater status."""
|
||||
|
||||
power: bool
|
||||
setpoint: int | None = None
|
||||
state: str | None = None # "Stop", "Heat", etc.
|
||||
error_code: int | None = None
|
||||
raw_dps: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ActionResult(BaseModel):
|
||||
"""Result of a heater action."""
|
||||
|
||||
success: bool
|
||||
action: str
|
||||
power: bool | None = None
|
||||
error: str | None = None
|
||||
Reference in New Issue
Block a user