From 5cf970df62fa9885436e1e7e26e98b2d3b76594d Mon Sep 17 00:00:00 2001 From: Luke Calladine Date: Mon, 12 Jan 2026 20:27:00 +0000 Subject: [PATCH] Add simple HanchuESS Helper --- .env.example | 24 ++++++++ backend/requirements.txt | 5 +- backend/src/main.py | 11 ++++ backend/src/service/hanchu_service.py | 80 +++++++++++++++++++++++++++ docker-compose.yml | 13 ++++- 5 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 backend/src/service/hanchu_service.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0f59a5b --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# HanchuESS API Configuration +# Copy this file to .env and fill in your actual values + +# Required: AES encryption key (must be 16, 24, or 32 bytes) +HANCHU_AES_KEY=your_aes_key_here + +# Required: AES initialization vector (must be 16 bytes) +HANCHU_AES_IV=your_aes_iv_here + +# Required: Login URL for the HanchuESS API +HANCHU_LOGIN_URL=https://api.example.com/login + +# Optional: Login type (default: ACCOUNT) +HANCHU_LOGIN_TYPE=ACCOUNT + +# Optional: HTTP timeout in seconds (default: 10) +HANCHU_HTTP_TIMEOUT=10 + +# Optional: Verify SSL certificates (default: true, set to false for self-signed certs) +HANCHU_VERIFY_SSL=true + +# Optional: Username and password +HANCHU_USERNAME= +HANCHU_PASSWORD= diff --git a/backend/requirements.txt b/backend/requirements.txt index 8e0578a..e88472a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,2 +1,5 @@ fastapi -uvicorn[standard] \ No newline at end of file +uvicorn[standard] +requests>=2.31.0 +pycryptodome>=3.20.0 +python-dotenv>=1.0.1 \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index 66cc0bc..fb0d721 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -6,6 +6,17 @@ app = FastAPI(title="HanchuESS Solar Backend API") def root(): return {"message": "Welcome to the HanchuESS Solar Backend API!"} +@app.get("/get_access_token", tags=["HanchuESS"]) +def get_access_token(): + from service.hanchu_service import HanchuESSService + + hanchu_service = HanchuESSService() + try: + access_token = hanchu_service.get_access_token() + return {"access_token": access_token} + except Exception as e: + return {"error": str(e)} + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8050) \ No newline at end of file diff --git a/backend/src/service/hanchu_service.py b/backend/src/service/hanchu_service.py new file mode 100644 index 0000000..9272dda --- /dev/null +++ b/backend/src/service/hanchu_service.py @@ -0,0 +1,80 @@ +import base64 +import json +import os +import requests + +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from dotenv import load_dotenv + + +load_dotenv() + + +class HanchuESSService: + def __init__(self): + self.name = "HanchuESS Service" + + # Load config from environment + self.aes_key = os.environ["HANCHU_AES_KEY"].encode("utf-8") + self.aes_iv = os.environ["HANCHU_AES_IV"].encode("utf-8") + self.login_url = os.environ["HANCHU_LOGIN_URL"] + self.login_type = os.getenv("HANCHU_LOGIN_TYPE", "ACCOUNT") + self.timeout = int(os.getenv("HANCHU_HTTP_TIMEOUT", "10")) + self.verify_ssl = os.getenv("HANCHU_VERIFY_SSL", "true").lower() == "true" + self.hanchu_username = os.getenv("HANCHU_USERNAME", "") + self.hanchu_password = os.getenv("HANCHU_PASSWORD", "") + + # Safety checks + if len(self.aes_key) not in (16, 24, 32): + raise ValueError("AES key must be 16, 24, or 32 bytes") + if len(self.aes_iv) != 16: + raise ValueError("AES IV must be exactly 16 bytes") + + def encrypt_payload(self, data: dict | str) -> str: + """ + Encrypt payload using AES-CBC and return base64 string. + """ + if not isinstance(data, str): + data = json.dumps(data, separators=(",", ":")) + + cipher = AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) + ciphertext = cipher.encrypt(pad(data.encode("utf-8"), AES.block_size)) + return base64.b64encode(ciphertext).decode("utf-8") + + def get_access_token(self) -> str: + """ + Authenticate with Hanchu ESS and return access token. + """ + + payload = { + "account": self.hanchu_username, + "password": self.hanchu_password, + "loginType": self.login_type, + } + + encrypted_payload = self.encrypt_payload(payload) + + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": "Mozilla/5.0", + } + + response = requests.post( + self.login_url, + json={"data": encrypted_payload}, + headers=headers, + timeout=self.timeout, + verify=self.verify_ssl, + ) + + response.raise_for_status() + result = response.json() + + try: + return result["data"]["accessToken"] + except (KeyError, TypeError): + raise RuntimeError( + f"Hanchu login failed: {json.dumps(result, ensure_ascii=False)}" + ) diff --git a/docker-compose.yml b/docker-compose.yml index c74fef8..3245747 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,4 +5,15 @@ services: dockerfile: docker/Dockerfile container_name: hanchuess-solar-backend ports: - - "8050:8050" \ No newline at end of file + - "8050:8050" + env_file: + - .env + environment: + - HANCHU_AES_KEY=${HANCHU_AES_KEY} + - HANCHU_AES_IV=${HANCHU_AES_IV} + - HANCHU_LOGIN_URL=${HANCHU_LOGIN_URL} + - HANCHU_LOGIN_TYPE=${HANCHU_LOGIN_TYPE:-ACCOUNT} + - HANCHU_HTTP_TIMEOUT=${HANCHU_HTTP_TIMEOUT:-10} + - HANCHU_VERIFY_SSL=${HANCHU_VERIFY_SSL:-true} + - HANCHU_USERNAME=${HANCHU_USERNAME:-} + - HANCHU_PASSWORD=${HANCHU_PASSWORD:-} \ No newline at end of file