Initial Commit

This commit is contained in:
2026-06-11 11:50:50 -04:00
commit d4a69c41be
2748 changed files with 80489 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
"""C.A.F.E. - Visual automation editor for Home Assistant."""
from __future__ import annotations
import logging
from pathlib import Path
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .panel import async_register_panel, async_unregister_panel
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the C.A.F.E. component."""
# This will be called when the integration is loaded
# But actual setup happens in async_setup_entry
return True
async def async_setup_entry(hass: HomeAssistant, entry) -> bool:
"""Set up C.A.F.E. from a config entry."""
# Register the panel (frontend)
await async_register_panel(hass)
_LOGGER.info("C.A.F.E. integration set up successfully")
return True
async def async_unload_entry(hass: HomeAssistant, entry) -> bool:
"""Unload a config entry."""
async_unregister_panel(hass)
return True
+53
View File
@@ -0,0 +1,53 @@
"""Config flow for C.A.F.E. integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema({})
class CafeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for C.A.F.E."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is not None:
# Check if already configured
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="C.A.F.E.",
data={}
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"name": "C.A.F.E.",
"description": "Visual automation editor for Home Assistant"
}
)
async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle import from configuration.yaml."""
await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="C.A.F.E.", data={})
+6
View File
@@ -0,0 +1,6 @@
"""Constants for C.A.F.E.."""
DOMAIN = "cafe"
PANEL_URL = "/cafe_panel"
PANEL_TITLE = "C.A.F.E."
PANEL_ICON = "mdi:coffee-to-go"
+14
View File
@@ -0,0 +1,14 @@
{
"domain": "cafe",
"name": "C.A.F.E.",
"version": "0.6.0",
"codeowners": [],
"config_flow": true,
"dependencies": ["frontend", "http"],
"documentation": "https://github.com/FezVrasta/cafe-hass",
"documentation_url": "https://github.com/FezVrasta/cafe-hass",
"integration_type": "service",
"iot_class": "local_push",
"issue_tracker": "https://github.com/FezVrasta/cafe-hass/issues",
"requirements": []
}
+66
View File
@@ -0,0 +1,66 @@
"""Panel for C.A.F.E."""
import os
import logging
from pathlib import Path
from homeassistant.core import HomeAssistant
from homeassistant.components import frontend, panel_custom
from homeassistant.components.http import StaticPathConfig
from .const import DOMAIN, PANEL_TITLE, PANEL_ICON
_LOGGER = logging.getLogger(__name__)
PANEL_URL = f"/api/{DOMAIN}_panel"
PANEL_NAME = f"{DOMAIN}-panel"
async def async_register_panel(hass: HomeAssistant) -> None:
"""Register the C.A.F.E. panel."""
# Get the path to the www folder within the component
www_path = Path(__file__).parent / "www"
# Register static path for the frontend assets
await hass.http.async_register_static_paths([
StaticPathConfig("/cafe-hass", str(www_path), False)
])
# The panel wrapper has a fixed name (not hashed)
wrapper_path = www_path / "assets" / "panel-wrapper.js"
if not wrapper_path.exists():
_LOGGER.error("panel-wrapper.js not found in assets directory")
_LOGGER.error(f"www path: {www_path}, assets exist: {(www_path / 'assets').exists()}")
return
# Add cache-busting parameter to force fresh load
import time
cache_bust = int(time.time())
module_url = f"/cafe-hass/assets/panel-wrapper.js?v={cache_bust}"
# First try to unregister any existing panel
try:
hass.data.get("frontend_panels", {}).pop(DOMAIN, None)
_LOGGER.info("Removed any existing panel registration")
except:
pass
# Register the panel as a custom panel with module_url
await panel_custom.async_register_panel(
hass,
webcomponent_name=PANEL_NAME,
frontend_url_path=DOMAIN,
module_url=module_url,
sidebar_title=PANEL_TITLE,
sidebar_icon=PANEL_ICON,
require_admin=True,
config={},
config_panel_domain=DOMAIN,
)
_LOGGER.info("C.A.F.E. panel registered successfully with iframe wrapper")
def async_unregister_panel(hass: HomeAssistant) -> None:
"""Unregister the C.A.F.E. panel."""
frontend.async_remove_panel(hass, DOMAIN)
_LOGGER.info("C.A.F.E. panel unregistered")
+14
View File
@@ -0,0 +1,14 @@
{
"config": {
"title": "C.A.F.E.",
"step": {
"user": {
"title": "Set up C.A.F.E.",
"description": "C.A.F.E. is a visual automation editor for Home Assistant. It allows you to create and manage your automations using a visual flow editor.\n\nClick Submit to add C.A.F.E. to your Home Assistant."
}
},
"abort": {
"already_configured": "C.A.F.E. is already configured"
}
}
}
@@ -0,0 +1,14 @@
{
"config": {
"title": "C.A.F.E.",
"step": {
"user": {
"title": "Set up C.A.F.E.",
"description": "C.A.F.E. is a visual automation editor for Home Assistant. It allows you to create and manage your automations using a visual flow editor.\n\nClick Submit to add C.A.F.E. to your Home Assistant."
}
},
"abort": {
"already_configured": "C.A.F.E. is already configured"
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
var d=Object.defineProperty;var o=(t,s,e)=>s in t?d(t,s,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[s]=e;var a=(t,s,e)=>o(t,typeof s!="symbol"?s+"":s,e);class m extends HTMLElement{constructor(){super(...arguments);a(this,"_messageHandler");a(this,"iframe",null);a(this,"_hass")}set hass(e){var i;this._hass=e,window.hass=e,(i=this.iframe)!=null&&i.contentWindow&&this.iframe.contentWindow.setHass&&this.iframe.contentWindow.setHass(e)}get hass(){return this._hass}connectedCallback(){var n,r;this.style.display="block",this.style.width="100%",this.style.height="100%",this.style.position="relative";const i=((r=(n=this._hass)==null?void 0:n.themes)==null?void 0:r.darkMode)??!1?"hsl(222.2, 84%, 4.9%)":"hsl(0, 0%, 100%)";this.iframe=document.createElement("iframe"),this.iframe.src="/cafe-hass/index.html",this.iframe.style.width="100%",this.iframe.style.height="100%",this.iframe.style.border="none",this.iframe.style.display="block",this.iframe.style.background=i,this.iframe.setAttribute("allow","clipboard-read *; clipboard-write *"),this.appendChild(this.iframe),this._messageHandler=h=>{var l;h.source===((l=this.iframe)==null?void 0:l.contentWindow)&&h.data&&h.data.type==="CAFE_TOGGLE_SIDEBAR"&&this.dispatchEvent(new Event("hass-toggle-menu",{bubbles:!0,composed:!0}))},window.addEventListener("message",this._messageHandler)}disconnectedCallback(){this.iframe&&(this.removeChild(this.iframe),this.iframe=null),window.hass=void 0,this._messageHandler&&(window.removeEventListener("message",this._messageHandler),this._messageHandler=void 0)}}customElements.get("cafe-panel")||customElements.define("cafe-panel",m);
//# sourceMappingURL=panel-wrapper.js.map
@@ -0,0 +1 @@
{"version":3,"file":"panel-wrapper.js","sources":["../../src/panel-wrapper.ts"],"sourcesContent":["/**\n * Minimal wrapper for Home Assistant panel integration.\n * This web component receives the hass object from HA, exposes it on window,\n * and loads the actual app in an iframe for proper document isolation.\n */\nimport type { HomeAssistant } from './types/hass';\n\n// Type for window with hass\ndeclare const window: Window & {\n hass?: HomeAssistant;\n};\n\nclass CafePanelWrapper extends HTMLElement {\n private _messageHandler?: (event: MessageEvent) => void;\n private iframe: HTMLIFrameElement | null = null;\n private _hass: HomeAssistant | undefined = undefined;\n\n // Properties that HA will set\n set hass(value: HomeAssistant | undefined) {\n this._hass = value;\n // Expose hass on window so iframe can access via window.parent.hass\n window.hass = value;\n\n // Notify the iframe of the update if it has registered a listener\n if (this.iframe?.contentWindow && (this.iframe.contentWindow as any).setHass) {\n (this.iframe.contentWindow as any).setHass(value);\n }\n }\n\n get hass() {\n return this._hass;\n }\n\n connectedCallback() {\n // Style the wrapper to fill the container\n this.style.display = 'block';\n this.style.width = '100%';\n this.style.height = '100%';\n this.style.position = 'relative';\n\n // Detect dark mode from hass to set initial background and avoid white flash\n const isDarkMode = this._hass?.themes?.darkMode ?? false;\n // These match the CSS variables in index.css:\n // Light: --background: 0 0% 100% (white)\n // Dark: --background: 222.2 84% 4.9% (dark blue)\n const bgColor = isDarkMode ? 'hsl(222.2, 84%, 4.9%)' : 'hsl(0, 0%, 100%)';\n\n // Create iframe pointing to the app\n this.iframe = document.createElement('iframe');\n this.iframe.src = '/cafe-hass/index.html';\n this.iframe.style.width = '100%';\n this.iframe.style.height = '100%';\n this.iframe.style.border = 'none';\n this.iframe.style.display = 'block';\n this.iframe.style.background = bgColor;\n // Allow same-origin access\n this.iframe.setAttribute('allow', 'clipboard-read *; clipboard-write *');\n\n this.appendChild(this.iframe);\n\n // Listen for messages from the iframe to trigger sidebar toggle\n this._messageHandler = (event: MessageEvent) => {\n // Only accept messages from our iframe\n if (event.source !== this.iframe?.contentWindow) return;\n if (event.data && event.data.type === 'CAFE_TOGGLE_SIDEBAR') {\n this.dispatchEvent(new Event('hass-toggle-menu', { bubbles: true, composed: true }));\n }\n };\n window.addEventListener('message', this._messageHandler);\n }\n\n disconnectedCallback() {\n if (this.iframe) {\n this.removeChild(this.iframe);\n this.iframe = null;\n }\n // Clean up window properties\n window.hass = undefined;\n if (this._messageHandler) {\n window.removeEventListener('message', this._messageHandler);\n this._messageHandler = undefined;\n }\n }\n}\n\n// Register the custom element\nif (!customElements.get('cafe-panel')) {\n customElements.define('cafe-panel', CafePanelWrapper);\n}\n"],"names":["CafePanelWrapper","__publicField","value","_a","_b","bgColor","event"],"mappings":"oKAYA,MAAMA,UAAyB,WAAY,CAA3C,kCACUC,EAAA,wBACAA,EAAA,cAAmC,MACnCA,EAAA,cAGR,IAAI,KAAKC,EAAkC,CAN7C,IAAAC,EAOI,KAAK,MAAQD,EAEb,OAAO,KAAOA,GAGVC,EAAA,KAAK,SAAL,MAAAA,EAAa,eAAkB,KAAK,OAAO,cAAsB,SAClE,KAAK,OAAO,cAAsB,QAAQD,CAAK,CAEpD,CAEA,IAAI,MAAO,CACT,OAAO,KAAK,KACd,CAEA,mBAAoB,CArBtB,IAAAC,EAAAC,EAuBI,KAAK,MAAM,QAAU,QACrB,KAAK,MAAM,MAAQ,OACnB,KAAK,MAAM,OAAS,OACpB,KAAK,MAAM,SAAW,WAOtB,MAAMC,IAJaD,GAAAD,EAAA,KAAK,QAAL,YAAAA,EAAY,SAAZ,YAAAC,EAAoB,WAAY,GAItB,wBAA0B,mBAGvD,KAAK,OAAS,SAAS,cAAc,QAAQ,EAC7C,KAAK,OAAO,IAAM,wBAClB,KAAK,OAAO,MAAM,MAAQ,OAC1B,KAAK,OAAO,MAAM,OAAS,OAC3B,KAAK,OAAO,MAAM,OAAS,OAC3B,KAAK,OAAO,MAAM,QAAU,QAC5B,KAAK,OAAO,MAAM,WAAaC,EAE/B,KAAK,OAAO,aAAa,QAAS,qCAAqC,EAEvE,KAAK,YAAY,KAAK,MAAM,EAG5B,KAAK,gBAAmBC,GAAwB,CAjDpD,IAAAH,EAmDUG,EAAM,WAAWH,EAAA,KAAK,SAAL,YAAAA,EAAa,gBAC9BG,EAAM,MAAQA,EAAM,KAAK,OAAS,uBACpC,KAAK,cAAc,IAAI,MAAM,mBAAoB,CAAE,QAAS,GAAM,SAAU,EAAA,CAAM,CAAC,CAEvF,EACA,OAAO,iBAAiB,UAAW,KAAK,eAAe,CACzD,CAEA,sBAAuB,CACjB,KAAK,SACP,KAAK,YAAY,KAAK,MAAM,EAC5B,KAAK,OAAS,MAGhB,OAAO,KAAO,OACV,KAAK,kBACP,OAAO,oBAAoB,UAAW,KAAK,eAAe,EAC1D,KAAK,gBAAkB,OAE3B,CACF,CAGK,eAAe,IAAI,YAAY,GAClC,eAAe,OAAO,aAAcN,CAAgB"}
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>C.A.F.E. - Home Assistant</title>
<script>
// Set background color immediately to avoid white flash in dark mode
// This runs before CSS loads, checking parent window's hass dark mode setting
(function() {
try {
var isDark = window.parent && window.parent.hass && window.parent.hass.themes && window.parent.hass.themes.darkMode;
// Match CSS variables: light = hsl(0, 0%, 100%), dark = hsl(222.2, 84%, 4.9%)
var bg = isDark ? 'hsl(222.2, 84%, 4.9%)' : 'hsl(0, 0%, 100%)';
document.documentElement.style.background = bg;
// Inject style for body since it doesn't exist yet
var style = document.createElement('style');
style.textContent = 'body { background: ' + bg + ' !important; }';
document.head.appendChild(style);
} catch (e) {}
})();
</script>
<script type="module" crossorigin src="/cafe-hass/assets/main-BIzscans.js"></script>
<link rel="stylesheet" crossorigin href="/cafe-hass/assets/main-DfXhj31a.css">
</head>
<body>
<div id="root"></div>
</body>
</html>