├── .github └── workflows │ ├── actions.yml │ └── main.yml ├── README.md ├── custom_components └── config_editor │ ├── __init__.py │ ├── config_flow.py │ └── manifest.json └── hacs.json /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: Hacs Actions 2 | 3 | on: 4 | push: 5 | pull_request: 6 | #schedule: 7 | # - cron: "0 0 * * *" 8 | #workflow_dispatch: 9 | 10 | jobs: 11 | validate-hacs: 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v3" 15 | - name: HACS validation 16 | uses: "hacs/action@main" 17 | with: 18 | category: "integration" 19 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | #schedule: 7 | # - cron: '0 0 * * 1' 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Home Assistant Configuration Editor Helper 2 | 3 | More info: 4 | https://github.com/junkfix/config-editor-card 5 | 6 | 7 | ![screenshot](https://github.com/junkfix/config-editor-card/raw/main/screenshot.png) 8 | 9 | --- 10 | 11 | Buy Me A Coffee 12 | -------------------------------------------------------------------------------- /custom_components/config_editor/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import voluptuous as vol 4 | from homeassistant.components import websocket_api 5 | from atomicwrites import AtomicWriter 6 | 7 | DOMAIN = 'config_editor' 8 | _LOGGER = logging.getLogger(__name__) 9 | 10 | 11 | async def async_setup(hass, config): 12 | websocket_api.async_register_command(hass, websocket_create) 13 | return True 14 | 15 | async def async_setup_entry(hass, entry): 16 | websocket_api.async_register_command(hass, websocket_create) 17 | return True 18 | 19 | @websocket_api.require_admin 20 | @websocket_api.async_response 21 | @websocket_api.websocket_command( 22 | { 23 | vol.Required("type"): DOMAIN+"/ws", 24 | vol.Required("action"): str, 25 | vol.Required("file"): str, 26 | vol.Required("data"): str, 27 | vol.Required("ext"): str, 28 | vol.Optional("depth", default=2): int 29 | } 30 | ) 31 | async def websocket_create(hass, connection, msg): 32 | action = msg["action"] 33 | ext = msg["ext"] 34 | cver = 5 35 | if ext not in ["yaml","py","json","conf","js","txt","log","css","jinja","all"]: 36 | ext = "yaml" 37 | 38 | def extok(e): 39 | if len(e)<2: 40 | return False 41 | return ( ext == 'all' or e.endswith("."+ext) ) 42 | 43 | def rec(p, q): 44 | r = [ 45 | f for f in os.listdir(p) if os.path.isfile(os.path.join(p, f)) and 46 | extok(f) 47 | ] 48 | for j in r: 49 | p = j if q == '' else os.path.join(q, j) 50 | listyaml.append(p) 51 | 52 | def drec(r, s): 53 | for d in os.listdir(r): 54 | v = os.path.join(r, d) 55 | if os.path.isdir(v): 56 | p = d if s == '' else os.path.join(s, d) 57 | if(p.count(os.sep) < msg["depth"]) and ( ext == 'all' or p != 'custom_components' ): 58 | rec(v, p) 59 | drec(v, p) 60 | 61 | yamlname = msg["file"].replace("../", "/").strip('/') 62 | 63 | if not extok(msg["file"]): 64 | yamlname = "temptest."+ext 65 | 66 | fullpath = hass.config.path(yamlname) 67 | if (action == 'load'): 68 | _LOGGER.info('Loading '+fullpath) 69 | content = '' 70 | res = 'Loaded' 71 | try: 72 | def read(): 73 | with open(fullpath, encoding="utf-8") as fdesc: 74 | return fdesc.read() 75 | content = await hass.async_add_executor_job(read) 76 | except: 77 | res = 'Reading Failed' 78 | _LOGGER.exception("Reading failed: %s", fullpath) 79 | finally: 80 | connection.send_result( 81 | msg["id"], 82 | {'msg': res+': '+fullpath, 'file': yamlname, 'data': content, 'ext': ext, 'cver': cver} 83 | ) 84 | 85 | elif (action == 'save'): 86 | _LOGGER.info('Saving '+fullpath) 87 | content = msg["data"] 88 | res = "Saved" 89 | try: 90 | dirnm = os.path.dirname(fullpath) 91 | if not os.path.isdir(dirnm): 92 | os.makedirs(dirnm, exist_ok=True) 93 | try: 94 | stat_res = os.stat(fullpath) 95 | mode = stat_res.st_mode 96 | uid = stat_res.st_uid 97 | gid = stat_res.st_gid 98 | except: 99 | mode = 0o666 100 | uid = 0 101 | gid = 0 102 | with AtomicWriter(fullpath, overwrite=True).open() as fdesc: 103 | fdesc.write(content) 104 | 105 | def permi(): 106 | with open(fullpath, 'a') as fdesc: 107 | try: 108 | os.fchmod(fdesc.fileno(), mode) 109 | os.fchown(fdesc.fileno(), uid, gid) 110 | except: 111 | pass 112 | await hass.async_add_executor_job(permi) 113 | 114 | except: 115 | res = "Saving Failed" 116 | _LOGGER.exception(res+": %s", fullpath) 117 | finally: 118 | connection.send_result( 119 | msg["id"], 120 | {'msg': res+': '+fullpath} 121 | ) 122 | 123 | elif (action == 'list'): 124 | dirnm = os.path.dirname(hass.config.path(yamlname)) 125 | listyaml = [] 126 | def reca(): 127 | rec(dirnm, '') 128 | await hass.async_add_executor_job(reca) 129 | if msg["depth"]>0: 130 | def dreca(): 131 | drec(dirnm, '') 132 | await hass.async_add_executor_job(dreca) 133 | if (len(listyaml) < 1): 134 | listyaml = ['list_error.'+ext] 135 | connection.send_result( 136 | msg["id"], 137 | {'msg': str(len(listyaml))+' File(s)', 'file': listyaml, 'ext': ext, 'cver': cver} 138 | ) 139 | -------------------------------------------------------------------------------- /custom_components/config_editor/config_flow.py: -------------------------------------------------------------------------------- 1 | from homeassistant.helpers import config_entry_flow 2 | 3 | config_entry_flow.register_discovery_flow( 4 | "config_editor", "Config Editor", lambda hass: True 5 | ) 6 | -------------------------------------------------------------------------------- /custom_components/config_editor/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "config_editor", 3 | "name": "Config Editor", 4 | "codeowners": ["@junkfix"], 5 | "config_flow": true, 6 | "documentation": "https://github.com/junkfix/config-editor-card", 7 | "iot_class": "local_polling", 8 | "issue_tracker": "https://github.com/junkfix/config-editor/issues", 9 | "version": "5.0.0" 10 | } 11 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Config Editor", 3 | "render_readme": true 4 | } 5 | --------------------------------------------------------------------------------