├── .github └── workflows │ └── validate.yml ├── README.md ├── config-editor-card.js ├── hacs.json └── screenshot.png /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 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: "plugin" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Config Editor Card to edit configuration.yaml from dashboard 3 | 4 | Automaticly searches for `*.yaml` in the root and subfolders and lists in the dropdown menu. 5 | 6 | 7 | ![screenshot](https://github.com/junkfix/config-editor-card/raw/main/screenshot.png) 8 | 9 | 10 | 11 | ## Installation 12 | 13 | ### Step 1 14 | You will **also** need to install a custom component https://github.com/junkfix/config-editor to reads/writes files 15 | 16 | * **[HACS](https://hacs.xyz/)** 17 | 18 | Integration > `Config Editor` 19 | 20 | 21 | * **Manual** 22 | 23 | Download and copy `config_editor` directory in `custom_components` 24 | 25 | Restart home assistant. 26 | 27 | Do one of the following: 28 | 29 | * Settings > Devices > Integrations > + Add Integration > Config Editor 30 | * Edit configuration.yaml and add the following so it can load 31 | ``` 32 | config_editor: 33 | ``` 34 | 35 | 36 | Restart home assistant. 37 | 38 | ### Step 2 39 | 40 | 41 | * **[HACS](https://hacs.xyz/)** 42 | 43 | Frontend > `Config Editor Card` 44 | 45 | * **Manual** 46 | 47 | Enable "Advanced Mode" from your user profile page 48 | 49 | add config-editor-card.js to your `/www/` folder 50 | 51 | Settings > Dashboards > ⋮ > Resources > + Add Resource > Javascript module > url `/local/config-editor-card.js?v=1` 52 | 53 | 54 | 55 | 56 | ## Add in the sidebar 57 | 58 | Settings > Dashboards > + Add dashboard 59 | 60 | create a new tab in panel mode and add the card [more info](https://github.com/junkfix/config-editor-card/issues/29) 61 | ```yaml 62 | type: custom:config-editor-card 63 | ``` 64 | 65 | 66 | It is also possible to add this using `+ Add Card` UI and choose `Custom: Config Editor Card` 67 | 68 | #### To create a new file 69 | choose the first blank in dropdown menu and type some text and hit Save or ctrl+s 70 | 71 | #### Advanced Config 72 | 73 | | Name | Default | Description 74 | | ---- | ------- | ----------- 75 | | file | | autoload file eg. `home-assistant.log` 76 | | readonly | `false` | read only 77 | | hidefooter | `false` | 78 | | basic | `false` | Force basic editor 79 | | size | `100` | font size 80 | | depth | `2` | subfolder depth 0 or more 81 | 82 | Please backup your files before using as there is no undo. 83 | --- 84 | 85 | Buy Me A Coffee 86 | -------------------------------------------------------------------------------- /config-editor-card.js: -------------------------------------------------------------------------------- 1 | console.info("Config Editor 5.0.0"); 2 | const LitElement = window.LitElement || Object.getPrototypeOf(customElements.get("hui-masonry-view") ); 3 | const html = LitElement.prototype.html; 4 | const css = LitElement.prototype.css; 5 | 6 | class ConfigEditor extends LitElement { 7 | 8 | static get properties() { 9 | return { 10 | _hass: {type: Object}, 11 | code: {type: String}, 12 | fileList: {type: Array}, 13 | openedFile: {type: String}, 14 | infoLine: {type: String}, 15 | alertLine: {type: String}, 16 | edit: {}, 17 | }; 18 | } 19 | 20 | constructor() { 21 | super(); 22 | this.code = ''; 23 | this.fileList = []; 24 | this.openedFile = ''; 25 | this.infoLine = ''; 26 | this.alertLine = ''; 27 | } 28 | 29 | static get styles() { 30 | return css` 31 | textarea{ 32 | width:98%; 33 | height:80vh; 34 | padding:5px; 35 | overflow-wrap:normal; 36 | white-space:pre} 37 | .top{ 38 | min-height:calc(95vh - var(--header-height,56px))} 39 | .pin,.filebar{ 40 | display:flex} 41 | .pin label{ 42 | cursor:pointer} 43 | .right{text-align:right; 44 | flex-grow:1} 45 | .right button{ 46 | font-family:Times,serif; 47 | font-weight:bold} 48 | .pin,.bar{ 49 | position:-webkit-sticky; 50 | position:sticky; 51 | z-index:2;} 52 | .pin{ 53 | top:0; 54 | background:var(--secondary-background-color,silver)} 55 | .bar{ 56 | bottom:0; 57 | z-index:2; 58 | background:var(--app-header-background-color,gray); 59 | color:var(--app-header-text-color,white); 60 | white-space:nowrap; 61 | overflow:hidden; 62 | text-overflow:ellipsis} 63 | .bar i{ 64 | background:#ff7a81; 65 | cursor:pointer} 66 | .bar select{ 67 | flex-grow:1; 68 | text-overflow:ellipsis; 69 | width:100%; 70 | overflow:hidden} 71 | `; 72 | } 73 | 74 | render(){ 75 | 76 | const targetver=5; 77 | 78 | if(!this._hass){return html``;} 79 | 80 | if(this.fileList.length<1){ 81 | this.openedFile = this.localGet('Open')||''; 82 | this.edit.ext = this.localGet('Ext')||'yaml'; 83 | this.edit.basic = this.localGet('Basic')||''; 84 | if(this.fileList = JSON.parse(this.localGet('List'+this.edit.ext))){ 85 | if(this.extOk(this.openedFile)){ 86 | setTimeout(this.oldText, 500, this); 87 | } 88 | }else{this.List();} 89 | } 90 | const hver=this.edit.cver; 91 | const dlink=html`download`; 92 | 93 | return html` 94 | 95 | ${(!this._hass.config.components.includes("config_editor")) ? html`
Missing 'config_editor:' in configuration.yaml ${dlink}
` : ''} 96 | ${(hver && hver != targetver) ? html`
Please ${dlink} upgrade from ${hver} to ${targetver}
` : ''} 97 |
98 |
99 |
100 |
101 | 102 | 103 | 104 | 109 | 111 |
112 |
113 | ${(this.edit.basic || this.edit.coder ) ? 114 | html``: 116 | html``} 119 |
120 | ${this.edit.hidefooter ? '' : html` 121 |
122 |
${this.alertLine}
123 |
${!this.edit.readonly ? 124 | html``:''} 125 | 130 | 131 |
132 | #${this.infoLine} 133 |
`} 134 |
135 | `; 136 | 137 | } 138 | 139 | txtSize(e){ 140 | if(e<3){ 141 | if(e>1){ 142 | this.edit.size=100; 143 | }else if(e>0){ 144 | this.edit.size+=5; 145 | }else{ 146 | this.edit.size-=5; 147 | } 148 | this.localSet('Size', this.edit.size); 149 | this.infoLine = 'size: '+this.edit.size; 150 | } 151 | this.renderRoot.querySelector('#code').style.fontSize=this.edit.size+'%'; 152 | } 153 | 154 | extChange(e){ 155 | this.edit.ext = e.target.value; 156 | this.localSet('Ext', this.edit.ext); 157 | this.openedFile = ''; 158 | this.oldText(this); 159 | this.List(); 160 | } 161 | 162 | basicChange(){ 163 | this.edit.basic = this.edit.basic?'':'1'; 164 | this.localSet('Basic', this.edit.basic); 165 | this.reLoad(); 166 | } 167 | 168 | updateText(e) { 169 | e.stopPropagation(); 170 | this.code = this.edit.basic ? e.target.value : e.detail.value; 171 | if(this.openedFile){this.localSet('Text', this.code);} 172 | } 173 | 174 | Unsave(){ 175 | this.code = this.localGet('Unsaved'); 176 | this.renderRoot.querySelector('#code').value=this.code; 177 | this.localSet('Unsaved',''); 178 | this.alertLine = ''; 179 | this.Toast("Loaded from browser",1500); 180 | } 181 | 182 | localGet(e){ 183 | return localStorage.getItem('config_editor'+e); 184 | } 185 | 186 | localSet(k,v){ 187 | localStorage.setItem('config_editor'+k,v); 188 | } 189 | 190 | cmd(action, data, file){ 191 | return this._hass.callWS({type: "config_editor/ws", action: action, 192 | data: data, file: file, ext: this.edit.ext, depth: this.edit.depth}); 193 | } 194 | 195 | saveList(){ 196 | this.localSet('List'+this.edit.ext, JSON.stringify(this.fileList)); 197 | } 198 | 199 | reLoad(e){ 200 | this.Load({target:{value:this.openedFile},reload:1}); 201 | } 202 | oldText(dhis){ 203 | dhis.Load({target:{value:dhis.openedFile}}); 204 | } 205 | 206 | saveKey(e) { 207 | if((e.key == 'S' || e.key == 's' ) && (e.ctrlKey || e.metaKey)){ 208 | e.preventDefault(); 209 | this.Save(); 210 | return false; 211 | } 212 | return true; 213 | } 214 | 215 | Toast(message, duration){ 216 | const e = new Event("hass-notification", 217 | {bubbles: true, cancelable: false, composed: true}); 218 | e.detail = {message, duration, dismissable: true, 219 | //action: {text:"Save",action:()=>{this.sureSave();}}, 220 | }; 221 | document.querySelector("home-assistant").dispatchEvent(e); 222 | } 223 | //sureSave(){console.log(this.openedFile);} 224 | 225 | async Coder(){ 226 | const c="ha-yaml-editor"; 227 | if(!customElements.get(c)){ 228 | await customElements.whenDefined("partial-panel-resolver"); 229 | const p = document.createElement('partial-panel-resolver'); 230 | p.hass = {panels: [{url_path: "tmp", component_name: "config"}]}; 231 | p._updateRoutes(); 232 | await p.routerOptions.routes.tmp.load(); 233 | const d=document.createElement("ha-panel-config"); 234 | await d.routerOptions.routes.automation.load(); 235 | } 236 | const a=document.createElement(c); 237 | this.edit.coder=0; 238 | if(!a){ 239 | this.localSet('Basic', 1); 240 | console.error('failed '+c); 241 | } 242 | this.render(); 243 | } 244 | 245 | async List(){ 246 | this.infoLine = 'List Loading...'; 247 | const e=await this.cmd('list','',''); 248 | if(e){ 249 | this.edit.cver = e.cver; 250 | this.infoLine = e.msg; 251 | this.fileList = e.file.slice().sort(); 252 | this.saveList(); 253 | if(this.extOk(this.openedFile)){ 254 | setTimeout(this.oldText, 500, this); 255 | } 256 | } 257 | } 258 | 259 | async Load(x) { 260 | if(x.target.value == this.openedFile && this.code && !x.hasOwnProperty('reload')){return;} 261 | if(this.edit.orgCode.trim() != this.code.trim()){ 262 | if(!confirm("Switch without Saving?")){x.target.value = this.openedFile; return;} 263 | } 264 | this.code = ''; this.renderRoot.querySelector('#code').value='';this.infoLine = ''; 265 | this.openedFile = x.target.value; 266 | if(this.openedFile){ 267 | this.infoLine = 'Loading: '+this.openedFile; 268 | const e=await this.cmd('load','',this.openedFile); 269 | if(e){ 270 | this.edit.cver = e.cver; 271 | this.openedFile = e.file; 272 | this.infoLine = e.msg; 273 | this.Toast(this.infoLine,1000); 274 | const uns={f:this.localGet('Open'), 275 | d:this.localGet('Text')}; 276 | if(uns.f == this.openedFile && uns.d && uns.d != e.data){ 277 | this.localSet('Unsaved', uns.d); 278 | this.alertLine = html`  279 | Load unsaved from browser `; 280 | }else{ 281 | this.localSet('Text','');this.alertLine = ''; 282 | } 283 | this.renderRoot.querySelector('#code').value=e.data; 284 | this.code = e.data; 285 | } 286 | } 287 | this.edit.orgCode = this.code; 288 | this.localSet('Open', this.openedFile); 289 | this.txtSize(3); 290 | } 291 | 292 | extOk(f){ 293 | if(f.length && (this.edit.ext=='all' || f.endsWith("."+this.edit.ext) )){return 1;} 294 | return 0; 295 | } 296 | 297 | async Save() { 298 | if(this.renderRoot.querySelector('#code').value != this.code || this.edit.readonly){ 299 | this.infoLine='Something not right!'; 300 | return; 301 | } 302 | let savenew=0; 303 | if(!this.openedFile && this.code){ 304 | this.openedFile=prompt("type abc."+this.edit.ext+" or folder/abc."+this.edit.ext); 305 | savenew=1; 306 | } 307 | if(this.extOk(this.openedFile)){ 308 | if(!confirm("Save?")){if(savenew){this.openedFile='';}return;} 309 | if(!this.code){this.infoLine=''; this.infoLine = 'Text is empty!'; return;} 310 | this.infoLine = 'Saving: '+this.openedFile; 311 | const e=await this.cmd('save', this.code, this.openedFile); 312 | if(e){ 313 | this.infoLine = e.msg; 314 | this.Toast(this.infoLine,2000); 315 | if(e.msg.includes('Saved:')){ 316 | this.localSet('Text',''); 317 | if(savenew){ 318 | this.fileList.unshift(this.openedFile); 319 | this.saveList(); 320 | } 321 | } 322 | } 323 | }else{this.openedFile='';} 324 | this.edit.orgCode = this.code; 325 | } 326 | 327 | getCardSize() { 328 | return 5; 329 | } 330 | 331 | setConfig(config) { 332 | this.edit = {file: '', hidefooter: false, readonly: false, basic: false, size: 0, depth: 2, ext: '', orgCode: '', coder: 1, cver: 0, ...config}; 333 | if(this.edit.file){ 334 | const f=this.edit.file.split('.')[1]; 335 | if(f){ 336 | this.localSet('Open', this.edit.file); 337 | this.localSet('Ext', f); 338 | } 339 | } 340 | if(!this.edit.size){ 341 | this.edit.size=Number(this.localGet('Size'))||100; 342 | } 343 | this.Coder(); 344 | } 345 | 346 | set hass(hass) { 347 | this._hass = hass; 348 | } 349 | 350 | shouldUpdate(changedProps) { 351 | for(const e of ['code','openedFile','fileList','infoLine','alertLine','edit']) { 352 | if(changedProps.has(e)){return true;} 353 | } 354 | } 355 | 356 | } customElements.define('config-editor-card', ConfigEditor); 357 | 358 | window.customCards = window.customCards || []; 359 | window.customCards.push({ 360 | type: 'config-editor-card', 361 | name: 'Config Editor Card', 362 | preview: false, 363 | description: 'Basic editor for configuration.yaml' 364 | }); 365 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Config Editor Card", 3 | "filename": "config-editor-card.js", 4 | "render_readme": true 5 | } -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkfix/config-editor-card/03413ea177ace3c349c4643ff5e0ce2c6ca2a552/screenshot.png --------------------------------------------------------------------------------