├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── app.js ├── haeditor.PNG ├── package.json ├── public ├── custom.css └── main.js └── views └── index.ejs /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vs/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | # Create app directory 3 | WORKDIR /usr/src/app 4 | # Install app dependencies 5 | COPY package.json . 6 | RUN npm install 7 | # Bundle app source 8 | COPY . . 9 | 10 | # Bind app port 11 | EXPOSE 3000 12 | 13 | # Define command to run the app 14 | CMD [ "node", "app" ] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ha-editor 2 | A web-based editor for home assistant based on the Monaco editor. 3 | 4 | 5 | 6 | ## Getting Started 7 | 8 | The editor is made to be run on the same system as home assistant and presented as a panel inside of the webpage of home assistant. 9 | 10 | Discussion at: 11 | https://community.home-assistant.io/t/web-based-editor/32051 12 | 13 |  14 | 15 | ### Prerequisites 16 | 17 | As of now the editor is served with nodejs. 18 | 19 | 20 | ### Installing 21 | 22 | #### Docker 23 | Install docker, for RaspberryPi see https://www.raspberrypi.org/blog/docker-comes-to-raspberry-pi/ 24 | 25 | Fetch the docker image 26 | ``` 27 | sudo docker pull voxic/ha-editor 28 | ``` 29 | 30 | Go to the location of home assistant configuration and start the docker container. 31 | ``` 32 | cd /location_of_homeassistant_configfiles 33 | sudo docker run -d -p 3000:3000 --mount type=bind,source="$(pwd)"/,target=/usr/src/app/configFolder voxic/ha-editor 34 | ``` 35 | 36 | Edit configuration.yaml and add the following: 37 | 38 | ``` 39 | # Enable the editor panel 40 | panel_iframe: 41 | editor: 42 | title: 'Editor' 43 | url: 'http://xxx.xxx.xxx.xxx:3000' #IP to your home assistant 44 | icon: mdi:book-open 45 | ``` 46 | 47 | Restart Home assistant 48 | 49 | #### Nodejs 50 | Make sure that you can run node and npm. 51 | 52 | Clone this repo. 53 | 54 | ``` 55 | git clone https://github.com/voxic/ha-editor.git 56 | 57 | ``` 58 | Go in to the folder and install NPM modules 59 | 60 | ``` 61 | cd ha-editor 62 | npm install 63 | ``` 64 | 65 | Edit app.js and the variable ```baseDir``` to point to the folder where you home assistant config files are located. 66 | 67 | ``` 68 | let baseDir = "configFolder" 69 | ``` 70 | 71 | Edit configuration.yaml and add the following: 72 | 73 | ``` 74 | # Enable the editor panel 75 | panel_iframe: 76 | editor: 77 | title: 'Editor' 78 | url: 'http://xxx.xxx.xxx.xxx:3000' #IP to your home assistant 79 | icon: mdi:book-open 80 | ``` 81 | 82 | Restart home assistant and start the web-editor 83 | ``` 84 | cd ha-editor 85 | node app.js 86 | ``` 87 | 88 | Enjoy! 89 | 90 | 91 | 92 | ## Built With 93 | 94 | * Monaco-editor 95 | * Bootstrap 96 | * Express 97 | * EJS 98 | * Jquery 99 | 100 | 101 | ## Authors 102 | 103 | * **Emil Nilsson** - [voxic](https://github.com/voxic) 104 | 105 | 106 | ## License 107 | 108 | This project is licensed under the MIT License - see the [LICENSE](http://rem.mit-license.org) for details. 109 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //load modules 4 | let express = require('express'); 5 | let app = express(); 6 | let fs = require('fs'); 7 | let bodyParser = require('body-parser'); 8 | 9 | //The root folder for homeassistant config files. 10 | let baseDir = "configFolder" 11 | 12 | 13 | //Function to load a file from disk and return it 14 | function getFile(filePath, fn){ 15 | fs.readFile(filePath, 'utf8', function(err, data) { 16 | if (err) throw err; 17 | return fn(data); 18 | }); 19 | } 20 | 21 | //Function to list files in a directory and return it as an array 22 | function getDir(dirPath, fn){ 23 | fs.readdir(dirPath, function(err,data){ 24 | let tempFiles = []; 25 | let tempFolders = [] 26 | if(data != null){ 27 | data.forEach(function(element) { 28 | //Check file type and only include known file types 29 | if(element.includes(".yaml")){ 30 | tempFiles.push(element); 31 | } 32 | else if(element.includes(".py")){ 33 | tempFiles.push(element); 34 | } 35 | else if(!element.includes(".")){ 36 | tempFolders.push(element); 37 | } 38 | }, this); 39 | } 40 | let filesDirs = {'folders': tempFolders, 'files': tempFiles, 'currentFolder' : dirPath }; 41 | return fn(filesDirs); 42 | }); 43 | } 44 | 45 | //Function to save the file to disk. 46 | function saveFile(filePath, data, fn){ 47 | fs.writeFile(filePath, data, (err) => { 48 | if (err) throw err; 49 | return fn('The file has been saved!'); 50 | }); 51 | } 52 | 53 | //set view engine to ejs 54 | app.set('view engine', 'ejs'); 55 | 56 | //set upp public directory to serve static files 57 | app.use(express.static('public')); 58 | app.use('/public', express.static(__dirname + '/public')); 59 | app.use('/bootstrap', express.static(__dirname + '/node_modules/bootstrap/dist/')); 60 | app.use('/jquery', express.static(__dirname + '/node_modules/jquery/dist/')); 61 | app.use('/monaco', express.static(__dirname + '/node_modules/monaco-editor/min/vs')); 62 | 63 | //Initiate bodyParser to parse request body 64 | app.use(bodyParser.urlencoded({ 65 | extended: true 66 | })); 67 | app.use(bodyParser.json()); 68 | 69 | //Routes 70 | app.get('/', (req, res) => { 71 | res.render('index'); 72 | }); 73 | 74 | app.post('/file', (req, res) => { 75 | getFile(req.body.path, (results)=>{ 76 | res.send(results); 77 | }); 78 | }); 79 | 80 | app.post('/fileList', (req, res) => { 81 | if(req.body.path == "home"){ 82 | getDir(baseDir, (results)=>{ 83 | res.send(results); 84 | }); 85 | } 86 | else { 87 | getDir(req.body.path, (results)=>{ 88 | res.send(results); 89 | }); 90 | } 91 | 92 | }); 93 | 94 | app.post('/saveFile', (req, res) => { 95 | saveFile(req.body.fileName, req.body.data, (results)=>{ 96 | res.sendStatus(200); 97 | }); 98 | }); 99 | 100 | // Run server 101 | console.log("Server is listening...") 102 | app.set('port', process.env.PORT || 3000); 103 | app.listen(app.get('port')); -------------------------------------------------------------------------------- /haeditor.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voxic/ha-editor/0585730d7ec4a90ebd2ce703250e7e0dcc6018a2/haeditor.PNG -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ha-editor", 3 | "version": "0.0.1", 4 | "description": "A monaco editor for homeassistant", 5 | "main": "app.js", 6 | "dependencies": { 7 | "body-parser": "^1.18.2", 8 | "bootstrap": "^3.3.7", 9 | "ejs": "^2.5.7", 10 | "express": "^4.16.2", 11 | "jquery": "^3.2.1", 12 | "monaco-editor": "^0.10.0" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "Emil Nilsson", 19 | "license": "MIT" 20 | } 21 | -------------------------------------------------------------------------------- /public/custom.css: -------------------------------------------------------------------------------- 1 | #editor { 2 | width: 95%; 3 | min-height: 500px; 4 | } 5 | 6 | #main { 7 | margin-top: 25px; 8 | } 9 | 10 | #menu { 11 | margin-top: 25px; 12 | } -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | var editor; 2 | var currentFile; 3 | var currentFolder = ""; 4 | let lastFolder = ""; 5 | let breadcrumb = []; 6 | 7 | 8 | //Function to load contents from a file on the server to the editor 9 | function loadFile(fileName){ 10 | let data = {"path" : currentFolder + "/" + fileName} 11 | $("#saveStatus").text(""); 12 | 13 | $.post( "/file", data, function() { 14 | }) 15 | .done(function(data) { 16 | $("#editorTitle").text(fileName); 17 | editor.setValue(data); 18 | currentFile = fileName; 19 | const regex = /\..+/g; 20 | let m; 21 | 22 | while ((m = regex.exec(fileName)) !== null) { 23 | // This is necessary to avoid infinite loops with zero-width matches 24 | if (m.index === regex.lastIndex) { 25 | regex.lastIndex++; 26 | } 27 | 28 | // The result can be accessed through the `m`-variable. 29 | m.forEach((match, groupIndex) => { 30 | if(match == ".py"){ 31 | monaco.editor.setModelLanguage(editor.getModel(), "python") 32 | console.log("Changed to python"); 33 | } 34 | else if(match == ".yaml"){ 35 | monaco.editor.setModelLanguage(editor.getModel(), "yaml") 36 | console.log("Changed to yaml"); 37 | } 38 | }); 39 | } 40 | }) 41 | .fail(function() { 42 | 43 | }) 44 | .always(function(data, status, xhr) { 45 | console.log(xhr.status); 46 | }); 47 | } 48 | 49 | 50 | //Function to save the contents from the editor to the server 51 | function saveFile(){ 52 | var data = {"fileName": currentFolder + "/" + currentFile, data: editor.getValue()}; 53 | $.post( "/saveFile", data, function() { 54 | 55 | }) 56 | .done(function(data) { 57 | 58 | $("#saveStatus").text("Saved!"); 59 | }) 60 | .fail(function(data, status, xhr) { 61 | console.log("API error " + status); 62 | }) 63 | .always(function() { 64 | 65 | }); 66 | } 67 | 68 | //Function to get a list of files from the server 69 | function getFiles(dirName){ 70 | $('#fileList').find("a").remove(); 71 | let data; 72 | 73 | if(dirName == null){ 74 | data = {"path" : "home"}; 75 | } 76 | else { 77 | data = {"path" : dirName} 78 | } 79 | 80 | $.post( "/fileList", data, function() { 81 | 82 | }) 83 | .done(function(data) { 84 | currentFolder = data.currentFolder; 85 | 86 | $("#currentFolder").text(currentFolder); 87 | $('#fileList').append(" Home"); 88 | if(data.length == 0){ 89 | $('#fileList').append("No files"); 90 | }else { 91 | $.each(data.folders, function (indexInArray, valueOfElement) { 92 | $('#fileList').append(" "+ valueOfElement + ""); 93 | }); 94 | $.each(data.files, function (indexInArray, valueOfElement) { 95 | $('#fileList').append(" "+ valueOfElement + ""); 96 | }); 97 | } 98 | 99 | }) 100 | .fail(function() { 101 | 102 | }) 103 | .always(function(data, status, xhr) { 104 | }); 105 | } 106 | 107 | $(function () { 108 | require.config({ paths: { 'vs': 'monaco' }}); 109 | require(['vs/editor/editor.main'], function() { 110 | editor = monaco.editor.create(document.getElementById('editor'), { 111 | value: "Ready, choose file to edit.", 112 | language: 'yaml' 113 | }); 114 | //Fetch files and dirs. 115 | getFiles(); 116 | 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |