├── .gitignore ├── LICENSE ├── README.md ├── build.cmd ├── custom_components └── panel_iframe │ ├── __init__.py │ ├── config_flow.py │ ├── http_proxy.py │ ├── manifest.json │ ├── manifest.py │ ├── translations │ └── en.json │ └── www │ ├── index.html │ ├── mdi │ ├── css │ │ ├── materialdesignicons.css │ │ ├── materialdesignicons.css.map │ │ ├── materialdesignicons.min.css │ │ └── materialdesignicons.min.css.map │ ├── fonts │ │ ├── materialdesignicons-webfont.eot │ │ ├── materialdesignicons-webfont.ttf │ │ ├── materialdesignicons-webfont.woff │ │ └── materialdesignicons-webfont.woff2 │ └── preview.html │ └── panel_iframe.js ├── hacs.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | .vscode/ 132 | node_modules/ 133 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 shaonianzhentan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 侧边栏面板 2 | 3 | [![hacs_badge](https://img.shields.io/badge/Home-Assistant-%23049cdb)](https://www.home-assistant.io/) 4 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) 5 | 6 | 7 | ![visit](https://visitor-badge.laobi.icu/badge?page_id=shaonianzhentan.panel_iframe&left_text=visit) 8 | 9 | ## 安装方式 10 | 11 | 安装完成重启HA,刷新一下页面,在集成里搜索`侧边栏面板`即可 12 | 13 | [![Add Integration](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=panel_iframe) 14 | 15 | 注意:长按侧边栏面板标题`Home Assistant`可隐藏菜单 16 | 17 | 18 | ## 使用方法 19 | 20 | 21 | 22 | **代理访问** 23 | 24 | - 内网访问地址:`http://localhost:1880/node-red/` 25 | - 代理访问地址:`http://HASS地址:8123/node-red/` 26 | 27 | ## 如果这个项目对你有帮助,请我喝杯咖啡奶茶吧😘 28 | | |支付宝|微信| 29 | |---|---|---| 30 | 奶茶= | 支付宝 | 微信支付 31 | 32 | 33 | #### 关注我的微信订阅号,了解更多HomeAssistant相关知识 34 | HomeAssistant家庭助理 -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | chcp 65001 2 | 3 | set SOURCE_DIR="node_modules/@mdi/font/" 4 | set TARGET_DIR="custom_components/panel_iframe/www/mdi/" 5 | 6 | echo SOURCE_DIR 7 | echo TARGET_DIR 8 | 9 | xcopy %SOURCE_DIR%preview.html %TARGET_DIR% /F /Y 10 | 11 | xcopy %SOURCE_DIR%css %TARGET_DIR%css /F /Y 12 | 13 | xcopy %SOURCE_DIR%fonts %TARGET_DIR%fonts /F /Y 14 | 15 | del package-lock.json -------------------------------------------------------------------------------- /custom_components/panel_iframe/__init__.py: -------------------------------------------------------------------------------- 1 | from homeassistant.config_entries import ConfigEntry 2 | from homeassistant.core import HomeAssistant 3 | from homeassistant.components.http import StaticPathConfig 4 | from homeassistant.components.panel_custom import async_register_panel 5 | import homeassistant.helpers.config_validation as cv 6 | import asyncio 7 | from .manifest import manifest 8 | from .http_proxy import HttpProxy 9 | 10 | DOMAIN = manifest.domain 11 | VERSION = manifest.version 12 | 13 | CONFIG_SCHEMA = cv.deprecated(DOMAIN) 14 | 15 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 16 | await hass.http.async_register_static_paths( 17 | [ StaticPathConfig("/panel_iframe_www", hass.config.path("custom_components/" + DOMAIN + "/www"), False) ] 18 | ) 19 | # 添加面板 20 | cfg = entry.options 21 | url_path = entry.entry_id 22 | title = entry.title 23 | mode = cfg.get('mode') 24 | icon = cfg.get('icon') 25 | url = cfg.get('url') 26 | require_admin = cfg.get('require_admin') 27 | proxy_access = cfg.get('proxy_access', False) 28 | 29 | if url is not None: 30 | module_url = f"/panel_iframe_www/panel_iframe.js?v={VERSION}" 31 | 32 | if proxy_access: 33 | proxy = HttpProxy(url) 34 | proxy.register(hass.http.app.router) 35 | url = proxy.get_url() 36 | 37 | await async_register_panel(hass, 38 | frontend_url_path=url_path, 39 | webcomponent_name="ha-panel_iframe", 40 | sidebar_title=title, 41 | sidebar_icon=icon, 42 | module_url=module_url, 43 | config={ 44 | 'mode': mode, 45 | 'url': url 46 | }, 47 | require_admin=require_admin 48 | ) 49 | entry.async_on_unload(entry.add_update_listener(update_listener)) 50 | return True 51 | 52 | async def update_listener(hass, entry): 53 | """Handle options update.""" 54 | await async_unload_entry(hass, entry) 55 | await asyncio.sleep(1) 56 | await async_setup_entry(hass, entry) 57 | 58 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 59 | url_path = entry.entry_id 60 | hass.components.frontend.async_remove_panel(url_path) 61 | # 移除路由监听 62 | return True -------------------------------------------------------------------------------- /custom_components/panel_iframe/config_flow.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | import voluptuous as vol 5 | 6 | from homeassistant.data_entry_flow import FlowResult 7 | 8 | import homeassistant.helpers.config_validation as cv 9 | from homeassistant.core import callback 10 | from homeassistant.config_entries import ConfigFlow, OptionsFlow, ConfigEntry 11 | 12 | from .manifest import manifest 13 | 14 | mode_list = { 15 | '0': '默认', 16 | '1': '全屏', 17 | '2': '新页面', 18 | '3': '内置页面' 19 | } 20 | 21 | class SimpleConfigFlow(ConfigFlow, domain=manifest.domain): 22 | 23 | VERSION = 1 24 | 25 | async def async_step_user( 26 | self, user_input: dict[str, Any] | None = None 27 | ) -> FlowResult: 28 | 29 | if user_input is None: 30 | errors = {} 31 | DATA_SCHEMA = vol.Schema({ 32 | vol.Required("title"): str, 33 | }) 34 | return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors) 35 | 36 | return self.async_create_entry(title=user_input['title'], data=user_input) 37 | 38 | @staticmethod 39 | @callback 40 | def async_get_options_flow(entry: ConfigEntry): 41 | return OptionsFlowHandler(entry) 42 | 43 | 44 | class OptionsFlowHandler(OptionsFlow): 45 | def __init__(self, config_entry: ConfigEntry): 46 | self.config_entry = config_entry 47 | 48 | async def async_step_init(self, user_input=None): 49 | return await self.async_step_user(user_input) 50 | 51 | async def async_step_user(self, user_input=None): 52 | errors = {} 53 | if user_input is None: 54 | options = self.config_entry.options 55 | errors = {} 56 | DATA_SCHEMA = vol.Schema({ 57 | vol.Required("icon", default=options.get('icon', 'mdi:link-box-outline')): str, 58 | vol.Required("url", default=options.get('url', '')): str, 59 | vol.Required("mode", default=options.get('mode', '0')): vol.In(mode_list), 60 | vol.Required("require_admin", default=options.get('require_admin', False)): bool, 61 | vol.Required("proxy_access", default=options.get('proxy_access', False)): bool, 62 | }) 63 | return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors) 64 | # 选项更新 65 | user_input['icon'] = user_input['icon'].strip().replace('mdi-', 'mdi:') 66 | user_input['url'] = user_input['url'].strip() 67 | 68 | # 内置页面禁止使用代理 69 | if user_input['mode'] == '3': 70 | user_input['proxy_access'] = False 71 | 72 | return self.async_create_entry(title='', data=user_input) -------------------------------------------------------------------------------- /custom_components/panel_iframe/http_proxy.py: -------------------------------------------------------------------------------- 1 | import aiohttp, asyncio 2 | from aiohttp import web 3 | from urllib.parse import urlparse 4 | 5 | class HttpProxy: 6 | 7 | def __init__(self, url: str): 8 | parsed_url = urlparse(url) 9 | route_path = parsed_url.path.strip('/') 10 | 11 | if route_path == '': 12 | route_path = parsed_url.netloc.replace(':', '').replace('.', '') 13 | self.is_root = True 14 | else: 15 | self.is_root = False 16 | self.proxy_host = parsed_url.netloc 17 | self.proxy_path = route_path 18 | 19 | def register(self, router): 20 | ''' 路由注册 ''' 21 | route_url = f'/{self.proxy_path}/' + '{tail:.*}' 22 | # print(route_url) 23 | router.add_route('*', route_url, self.handler) 24 | 25 | def get_url(self, hostname=''): 26 | ''' 获取访问地址 ''' 27 | return f'{hostname}/{self.proxy_path}/' 28 | 29 | def get_path(self, request): 30 | ''' 获取真实路径地址 ''' 31 | url_path = request.rel_url.path 32 | if self.is_root: 33 | url_path = url_path.replace(f'/{self.proxy_path}', '') 34 | return url_path 35 | 36 | async def handler(self, request): 37 | target_ws = f'ws://{self.proxy_host}' 38 | target_http = f'http://{self.proxy_host}' 39 | if request.headers.get('Upgrade', '').lower() == 'websocket': 40 | return await self.websocket_handler(request, target_ws) 41 | else: 42 | return await self.http_handler(request, target_http) 43 | 44 | async def http_handler(self, request, target_url): 45 | 46 | target = target_url + self.get_path(request) 47 | if request.query_string: 48 | target += '?' + request.query_string 49 | 50 | async with aiohttp.ClientSession() as session: 51 | async with session.request( 52 | method=request.method, 53 | url=target, 54 | headers={k: v for k, v in request.headers.items() if k.lower() != 'host'}, 55 | data=await request.read() 56 | ) as resp: 57 | headers = {k: v for k, v in resp.headers.items() if k.lower() != 'transfer-encoding'} 58 | body = await resp.read() 59 | return web.Response(body=body, status=resp.status, headers=headers) 60 | 61 | async def websocket_handler(self, request, target_url): 62 | ws_server = web.WebSocketResponse() 63 | await ws_server.prepare(request) 64 | 65 | target = target_url + self.get_path(request) 66 | async with aiohttp.ClientSession() as session: 67 | async with session.ws_connect(target) as ws_client: 68 | async def ws_forward(ws_from, ws_to): 69 | async for msg in ws_from: 70 | if msg.type == aiohttp.WSMsgType.TEXT: 71 | await ws_to.send_str(msg.data) 72 | elif msg.type == aiohttp.WSMsgType.BINARY: 73 | await ws_to.send_bytes(msg.data) 74 | elif msg.type == aiohttp.WSMsgType.CLOSE: 75 | await ws_to.close() 76 | 77 | await asyncio.gather(ws_forward(ws_server, ws_client), ws_forward(ws_client, ws_server)) 78 | 79 | return ws_server -------------------------------------------------------------------------------- /custom_components/panel_iframe/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "panel_iframe", 3 | "name": "\u4FA7\u8FB9\u680F\u9762\u677F", 4 | "version": "2024.7.30", 5 | "config_flow": true, 6 | "documentation": "/panel_iframe_www/mdi/preview.html", 7 | "dependencies": ["frontend"], 8 | "codeowners": ["@shaonianzhentan"], 9 | "quality_scale": "internal" 10 | } -------------------------------------------------------------------------------- /custom_components/panel_iframe/manifest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from homeassistant.util.json import load_json 3 | 4 | CURRENT_PATH = os.path.dirname(__file__) 5 | 6 | class Manifest(): 7 | 8 | def __init__(self): 9 | self.manifest_path = f'{CURRENT_PATH}/manifest.json' 10 | self.update() 11 | 12 | def update(self): 13 | data = load_json(self.manifest_path, {}) 14 | self.domain = data.get('domain') 15 | self.name = data.get('name') 16 | self.version = data.get('version') 17 | self.documentation = data.get('documentation') 18 | 19 | manifest = Manifest() -------------------------------------------------------------------------------- /custom_components/panel_iframe/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "侧边栏面板", 6 | "description": "项目:https://github.com/shaonianzhentan/panel_iframe", 7 | "data": { 8 | "title": "名称" 9 | } 10 | } 11 | }, 12 | "error": {}, 13 | "abort": {} 14 | }, 15 | "options": { 16 | "step": { 17 | "user": { 18 | "title": "侧边栏面板", 19 | "description": "点击问号图标,查看所有可选图标,点击复制直接使用", 20 | "data": { 21 | "icon": "图标", 22 | "url": "链接", 23 | "require_admin": "管理员可见", 24 | "mode": "显示模式", 25 | "proxy_access": "代理访问(不懂勿选)" 26 | } 27 | } 28 | }, 29 | "error": {} 30 | } 31 | } -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 侧边栏管理 9 | 92 | 93 | 94 | 95 |
96 |
Loading...
97 | https://github.com/shaonianzhentan 98 | github.com 99 |
100 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/panel_iframe/a43f6ca12cbb13e32c40c624ed26aeaefbb4d32e/custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/panel_iframe/a43f6ca12cbb13e32c40c624ed26aeaefbb4d32e/custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/panel_iframe/a43f6ca12cbb13e32c40c624ed26aeaefbb4d32e/custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaonianzhentan/panel_iframe/a43f6ca12cbb13e32c40c624ed26aeaefbb4d32e/custom_components/panel_iframe/www/mdi/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /custom_components/panel_iframe/www/panel_iframe.js: -------------------------------------------------------------------------------- 1 | customElements.whenDefined('ha-panel-lovelace').then(() => { 2 | const LitElement = Object.getPrototypeOf(customElements.get("ha-panel-lovelace")); 3 | const html = LitElement.prototype.html; 4 | const css = LitElement.prototype.css; 5 | 6 | customElements.define('ha-panel_iframe', class extends LitElement { 7 | 8 | static properties = { 9 | panel: {}, 10 | hass: {}, 11 | narrow: {} 12 | }; 13 | 14 | constructor() { 15 | super(); 16 | } 17 | 18 | static styles = css` 19 | :host { 20 | display: block; 21 | height: 100%; 22 | background-color: var(--primary-background-color); 23 | overflow: hidden; 24 | position: relative; 25 | } 26 | 27 | :host([narrow]) { 28 | width: 100%; 29 | position: fixed; 30 | } 31 | 32 | .toolbar { 33 | display: flex; 34 | align-items: center; 35 | font-size: 20px; 36 | height: var(--header-height); 37 | padding: 8px 12px; 38 | pointer-events: none; 39 | background-color: var(--app-header-background-color); 40 | font-weight: 400; 41 | color: var(--app-header-text-color, white); 42 | border-bottom: var(--app-header-border-bottom, none); 43 | box-sizing: border-box; 44 | } 45 | @media (max-width: 599px) { 46 | .toolbar { 47 | padding: 4px; 48 | } 49 | } 50 | .toolbar a { 51 | color: var(--sidebar-text-color); 52 | text-decoration: none; 53 | } 54 | 55 | ha-menu-button, 56 | ha-icon-button-arrow-prev, 57 | ::slotted([slot="toolbar-icon"]) { 58 | pointer-events: auto; 59 | } 60 | 61 | .main-title { 62 | margin: 0 0 0 24px; 63 | line-height: 20px; 64 | flex-grow: 1; 65 | } 66 | 67 | .content { 68 | position: relative; 69 | width: 100%; 70 | height: calc(100% - 1px - var(--header-height)); 71 | } 72 | 73 | #fab { 74 | position: absolute; 75 | right: calc(16px + env(safe-area-inset-right)); 76 | bottom: calc(16px + env(safe-area-inset-bottom)); 77 | z-index: 1; 78 | } 79 | :host([narrow]) #fab.tabs { 80 | bottom: calc(84px + env(safe-area-inset-bottom)); 81 | } 82 | #fab[is-wide] { 83 | bottom: 24px; 84 | right: 24px; 85 | } 86 | :host([rtl]) #fab { 87 | right: auto; 88 | left: calc(16px + env(safe-area-inset-left)); 89 | } 90 | :host([rtl][is-wide]) #fab { 91 | bottom: 24px; 92 | left: 24px; 93 | right: auto; 94 | } 95 | 96 | iframe{border:none;width:100%;height:100%;} 97 | .nav-button{ 98 | position:fixed; 99 | bottom:5px; 100 | right: 5px; 101 | }`; 102 | 103 | fireEvent(type, data) { 104 | const event = new Event(type, { 105 | bubbles: true, 106 | cancelable: false, 107 | composed: true 108 | }); 109 | event.detail = data; 110 | this.dispatchEvent(event); 111 | } 112 | 113 | _toggleMenu(event) { 114 | this.fireEvent('hass-toggle-menu') 115 | event.stopPropagation() 116 | } 117 | 118 | render() { 119 | const { config, title } = this.panel 120 | let { url, mode } = config 121 | // 如果传入的是端口号 122 | if (/^\d+$/.test(url)) { 123 | url = 'http://' + location.hostname + ':' + url 124 | } 125 | // 如果传入的双斜杠,则忽略默认端口 126 | if (url.indexOf('//') == 0) { 127 | url = location.protocol + '//' + location.hostname + url.substring(1) 128 | } 129 | // 如果传入端口路径 130 | if (url.indexOf(':') == 0) { 131 | url = location.protocol + '//' + location.hostname + url 132 | } 133 | // 内置页面 134 | if (mode == 3) { 135 | history.replaceState(null, null, url) 136 | return this.fireEvent('location-changed', { replace: true }) 137 | } 138 | // 如果HTTPS协议,则打开新页面 139 | if (location.protocol === 'https:' && url.indexOf('http://') == 0) { 140 | 141 | } else { 142 | // 全屏显示 143 | if (mode == 1) { 144 | return html` 145 | ${this.narrow ? html`` : ''}` 146 | } 147 | } 148 | // 内置显示 149 | return html`
150 | 154 |
${title}
155 | 156 |
157 |
158 | 159 |
160 | `; 161 | } 162 | }); 163 | }) -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "侧边栏面板", 3 | "country": "CN", 4 | "render_readme": true, 5 | "domains": [], 6 | "homeassistant": "2024.7.0" 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@mdi/font": "^7.4.47" 4 | }, 5 | "name": "panel_iframe", 6 | "description": "[![hacs_badge](https://img.shields.io/badge/Home-Assistant-%23049cdb)](https://www.home-assistant.io/)\r [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)", 7 | "version": "1.0.0", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "npm install @mdi/font & build.cmd" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@gitee.com:shaonianzhentan/panel_iframe.git" 16 | }, 17 | "author": "", 18 | "license": "ISC" 19 | } 20 | --------------------------------------------------------------------------------