Initial Commit
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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={})
|
||||
@@ -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"
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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"}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user