let haStateURL = "http://beacon:1880/ha_state" let metrics = { roomDefaultOpacity : 0.15, roomHighlightOpacity : 0.6, roomHighlightDuration : 0.3, roomUnhighlightDuration: 0.3, animationCurve : TWEEN.Easing.Cubic.InOut } const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) let homeContainer = new THREE.Group() scene.add(homeContainer) let home = new THREE.Group() homeContainer.add(home) var areaData = { } var areasWithEntitiesToUpdate = { } const renderer = new THREE.WebGLRenderer({ logarithmicDepthBuffer : true }) renderer.setSize( window.innerWidth, window.innerHeight ) document.body.appendChild( renderer.domElement ) const light = new THREE.AmbientLight(0xFFAAAA, 1) light.position.z = 100 scene.add(light) scene.background = new THREE.Color("white") // back the camera up camera.position.z = 25 // flip home on x axis home.rotation.x = Math.PI // Rotate container a bit for comfort homeContainer.rotation.x = 0.6 * (-Math.PI / 2) // Returns the first child (or container) whose name matches function childWithName(container, name) { if (container.userData.name == name) { return container; } for (var child of container.children) { let childMatches = childWithName(child, name) if (childMatches != null) { return childMatches } } return null } function animate() { requestAnimationFrame( animate ) TWEEN.update() var areasToUpdate = new Set() for (var area of Object.keys(areasWithEntitiesToUpdate)) { let updatedEntities = areasWithEntitiesToUpdate[area] if (updatedEntities.length > 0) { areasToUpdate.add(area) } } for (var area of areasToUpdate) { let areaContainer = childWithName(scene, area) let box = childWithName(areaContainer, "box") let fadeIn = new TWEEN.Tween(box.material) .easing(metrics.animationCurve) .to( { opacity: metrics.roomHighlightOpacity }, metrics.roomHighlightDuration * 1000) let fadeOut = new TWEEN.Tween(box.material) .easing(metrics.animationCurve) .to( { opacity: metrics.roomDefaultOpacity }, metrics.roomUnhighlightDuration * 1000) fadeIn.chain(fadeOut) fadeIn.start() // This will rotate to the area that has a change let rotate = new TWEEN.Tween(homeContainer.rotation) .easing(metrics.animationCurve) .to( { z: Math.PI }, 2000 ) .start() } homeContainer.rotation.z += 0.003 renderer.render( scene, camera ) } animate() function createTextMesh(text, colorName) { let canvas = document.createElement('canvas') let context = canvas.getContext('2d') let size = context.measureText(text) canvas.width = 100 canvas.height = 100 context.textAlign = "center" context.textBaseline = "middle" context.fillStyle = colorName context.font = '24pt sans-serif' context.fillText(text, 50, 50) let texture = new THREE.CanvasTexture(canvas) texture.needsUpdate = true let material = new THREE.MeshBasicMaterial({ map: texture }) material.transparent = true let plane = new THREE.PlaneGeometry(3, 3) let mesh = new THREE.Mesh(plane, material) return mesh } var lastUpdateTime = new Date() function updateWithHAData(data) { // `data` is an array of entities. loop through and group by area var areasToEntities = { } var areasToUpdatedEntities = { } var areaNames = new Array() for (var area of areaData.rooms) { let name = area.name areaNames.push(area.name) areasToEntities[name] = new Array() areasToUpdatedEntities[name] = new Array() } let sortedAreaNames = areaNames sortedAreaNames.sort(function(a, b) { return b.length - a.length; }) for (var entity of data) { let id = entity.entity_id let friendlyName = entity.attributes.friendly_name || entity.entity_id for (var area of sortedAreaNames) { let matchesUnderscored = id.includes(area.toLowerCase().replace(' ','_')) let matchesNoSpaces = id.includes(area.toLowerCase().replace(' ','')) let matchesFriendlyName = friendlyName.includes(area) let matches = matchesUnderscored || matchesNoSpaces || matchesFriendlyName if (matches) { let areaEntities = areasToEntities[area] areaEntities.push(entity) areasToEntities[area] = areaEntities let lastUpdate = new Date(entity.last_changed) if (lastUpdate > lastUpdateTime) { let areaUpdatedEntities = areasToUpdatedEntities[area] areaUpdatedEntities.push(entity) areasToUpdatedEntities[area] = areaUpdatedEntities } } } } lastUpdateTime = new Date() areasWithEntitiesToUpdate = areasToUpdatedEntities } async function loadHAData() { let request = new Request(haStateURL) fetch(request) .then(response => { return response.json() }) .then(json => { updateWithHAData(json) new Promise(resolve => setTimeout(resolve, 250)) .then(_ => { loadHAData() }) }) } function setPosition(mesh, x, y, z, w, h, d) { // position sets the *center* position // our coordinates are top/left/bottom let meshX = x + w/2 let meshY = y + h/2 let meshZ = -z - d/2 mesh.position.x = meshX mesh.position.y = meshY mesh.position.z = meshZ } function configureScene(data) { var minX = 1000 var maxX = -1000 var minY = minX var maxY = maxX areaData = data // Add geometry for the rooms for (var room of data.rooms) { let roomContainer = new THREE.Group() roomContainer.userData.name = room.name let roomGeo = new THREE.BoxGeometry(room.w, room.h, room.d) let roomColor = new THREE.Color(room.color) let roomBoxMaterial = new THREE.MeshPhysicalMaterial({ color: roomColor }) roomBoxMaterial.transparent = true roomBoxMaterial.opacity = metrics.roomDefaultOpacity let roomMesh = new THREE.Mesh(roomGeo, roomBoxMaterial) roomMesh.userData.name = "box" roomContainer.add(roomMesh) let roomEdgesGeo = new THREE.EdgesGeometry(roomGeo) let roomLinesMaterial = new THREE.LineBasicMaterial({ color: roomColor }) roomLinesMaterial.linewidth = 3 let roomLines = new THREE.LineSegments(roomEdgesGeo, roomLinesMaterial) roomLines.userData.name = "lines" roomContainer.add(roomLines) let roomLabel = createTextMesh("A test or something", "red") roomLabel.userData.name = "label" roomContainer.add(roomLabel) setPosition(roomContainer, room.x, room.y, room.z || 0, room.w, room.h, room.d) home.add(roomContainer) minX = Math.min(minX, room.x) minY = Math.min(minY, room.y) maxX = Math.max(maxX, room.x + room.w) maxY = Math.max(maxY, room.y + room.h) } let xExtent = maxX - minX let yExtent = maxY - minY home.position.x = -1 * (xExtent / 2) home.position.y = +1 * (yExtent / 2) } async function loadHomeData() { let request = new Request('data.json') fetch(request) .then(response => { return response.json() }) .then(json => { configureScene(json) // Kick off first HA load loadHAData() }) } loadHomeData()