Skip to content

Commit

Permalink
Add support for RGB lights
Browse files Browse the repository at this point in the history
Closes #22
  • Loading branch information
shmuelzon committed Sep 13, 2024
1 parent d1e2429 commit 1c52481
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 11 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ the room they're located in. Please verify the list matches your expectations.
* Light mixing mode - See [Rendering Modes](#rendering-modes)
* Sensitivity - [1, 100] The degree by which two pixels need to be different
from one another to be taken into account for the generated overlay image.
Only relevant for "room overlay" light mixing mode
Only relevant for "room overlay" light mixing mode and RGB lights
* Renderer - Select which rendering engine to use, YafaRay or SunFlow
* Quality - Choose the rendering quality (low or high)
* Render Time - The date and time of the rendered image, affects the sun
Expand All @@ -109,6 +109,13 @@ that allow you to customize the entity according to your needs.
[Tap Action](https://www.home-assistant.io/dashboards/actions/#tap-action)
* Always on - Only for lights, set light as always on removing its icon and it
won't be affected by the matching Home Assistant state
* Is RGB(W) light - Only for lights, set light as an RGB light that will change
its color in the floorplan according to the color set in Home Assistant. This
requires installing the
[config-template-card](https://github.com/iantrich/config-template-card)
custom Lovelace card.

:warning: **Note:** RGB lights are only supported in the CSS rendering mode.

## Preparation

Expand Down
Binary file modified doc/entityOptions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ HomeAssistantFloorPlan.Panel.tapActionComboBox.MORE_INFO.text=More info
HomeAssistantFloorPlan.Panel.tapActionComboBox.NONE.text=None
HomeAssistantFloorPlan.Panel.tapActionComboBox.TOGGLE.text=Toggle
HomeAssistantFloorPlan.Panel.alwaysOnLabel.text=Always on:
HomeAssistantFloorPlan.Panel.isRgbLabel.text=Is RGB(W) light:

HomeAssistantFloorPlan.Panel.attributes.alwaysOn.text=always on
HomeAssistantFloorPlan.Panel.attributes.isRgb.text=RGB

HomeAssistantFloorPlan.Panel.closeButton.text=Close
HomeAssistantFloorPlan.Panel.startButton.text=Start
Expand Down
108 changes: 100 additions & 8 deletions src/com/shmuelzon/HomeAssistantFloorPlan/Controller.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.shmuelzon.HomeAssistantFloorPlan;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
Expand Down Expand Up @@ -36,13 +37,15 @@


public class Controller {
public enum Property {COMPLETED_RENDERS, NUMBER_OF_RENDERS}
public enum Property {COMPLETED_RENDERS, NUMBER_OF_RENDERS, ENTITY_ATTRIBUTE_CHANGED}
public enum LightMixingMode {CSS, OVERLAY, FULL}
public enum Renderer {YAFARAY, SUNFLOW}
public enum Quality {HIGH, LOW}
public enum EntityDisplayType {BADGE, ICON, LABEL, NONE}
public enum EntityTapAction {MORE_INFO, NONE, TOGGLE}

private static final String TRANSPARENT_FILE_NAME = "transparent.png";

private static final String CONTROLLER_RENDER_WIDTH = "renderWidth";
private static final String CONTROLLER_RENDER_HEIGHT = "renderHeigh";
private static final String CONTROLLER_LIGHT_MIXING_MODE = "lightMixingMode";
Expand All @@ -55,6 +58,7 @@ public enum EntityTapAction {MORE_INFO, NONE, TOGGLE}
private static final String CONTROLLER_ENTITY_DISPLAY_TYPE = "displayType";
private static final String CONTROLLER_ENTITY_TAP_ACTION = "tapAction";
private static final String CONTROLLER_ENTITY_ALWAYS_ON = "alwaysOn";
private static final String CONTROLLER_ENTITY_IS_RGB = "isRgb";

private Home home;
private Settings settings;
Expand Down Expand Up @@ -88,6 +92,7 @@ private class Entity {
public EntityTapAction tapAction;
public String title;
public boolean alwaysOn;
public boolean isRgb;

public Entity(String name, Point2d position, EntityDisplayType defaultDisplayType, EntityTapAction defaultTapAction, String title) {
this.name = name;
Expand All @@ -96,6 +101,7 @@ public Entity(String name, Point2d position, EntityDisplayType defaultDisplayTyp
this.tapAction = EntityTapAction.valueOf(settings.get(name + "." + CONTROLLER_ENTITY_TAP_ACTION, defaultTapAction.name()));
this.title = title;
this.alwaysOn = settings.getBoolean(name + "." + CONTROLLER_ENTITY_ALWAYS_ON, false);
this.isRgb = settings.getBoolean(name + "." + CONTROLLER_ENTITY_IS_RGB, false);
}
}

Expand Down Expand Up @@ -266,19 +272,37 @@ public boolean getEntityAlwaysOn(String entityName) {
}

public void setEntityAlwaysOn(String entityName, boolean alwaysOn) {
int oldNumberOfTotaleRenders = getNumberOfTotalRenders();
int oldNumberOfTotalRenders = getNumberOfTotalRenders();
boolean oldAlwaysOn = homeAssistantEntities.get(entityName).alwaysOn;
homeAssistantEntities.get(entityName).alwaysOn = alwaysOn;
settings.setBoolean(entityName + "." + CONTROLLER_ENTITY_ALWAYS_ON, alwaysOn);
propertyChangeSupport.firePropertyChange(Property.NUMBER_OF_RENDERS.name(), oldNumberOfTotaleRenders, getNumberOfTotalRenders());
propertyChangeSupport.firePropertyChange(Property.NUMBER_OF_RENDERS.name(), oldNumberOfTotalRenders, getNumberOfTotalRenders());
propertyChangeSupport.firePropertyChange(Property.ENTITY_ATTRIBUTE_CHANGED.name(), oldAlwaysOn, alwaysOn);
}

public boolean getEntityIsRgb(String entityName) {
return homeAssistantEntities.get(entityName).isRgb;
}

public void setEntityIsRgb(String entityName, boolean isRgb) {
boolean oldIsRgb = homeAssistantEntities.get(entityName).isRgb;
homeAssistantEntities.get(entityName).isRgb = isRgb;
settings.setBoolean(entityName + "." + CONTROLLER_ENTITY_IS_RGB, isRgb);
propertyChangeSupport.firePropertyChange(Property.ENTITY_ATTRIBUTE_CHANGED.name(), oldIsRgb, isRgb);
}

public void resetEntitySettings(String entityName) {
boolean oldAlwaysOn = homeAssistantEntities.get(entityName).alwaysOn;
boolean oldIsRgb = homeAssistantEntities.get(entityName).isRgb;
settings.set(entityName + "." + CONTROLLER_ENTITY_DISPLAY_TYPE, null);
settings.set(entityName + "." + CONTROLLER_ENTITY_TAP_ACTION, null);
settings.set(entityName + "." + CONTROLLER_ENTITY_ALWAYS_ON, null);
settings.set(entityName + "." + CONTROLLER_ENTITY_IS_RGB, null);
int oldNumberOfTotaleRenders = getNumberOfTotalRenders();
homeAssistantEntities = generateHomeAssistantEntities();
propertyChangeSupport.firePropertyChange(Property.NUMBER_OF_RENDERS.name(), oldNumberOfTotaleRenders, getNumberOfTotalRenders());
propertyChangeSupport.firePropertyChange(Property.ENTITY_ATTRIBUTE_CHANGED.name(), oldAlwaysOn, getEntityAlwaysOn(entityName));
propertyChangeSupport.firePropertyChange(Property.ENTITY_ATTRIBUTE_CHANGED.name(), oldIsRgb, getEntityIsRgb(entityName));
}

public void stop() {
Expand All @@ -302,6 +326,7 @@ public void render() throws IOException, InterruptedException {
camera.setTime(renderDateTime);

String yaml = generateBaseRender();
generateTransparentImage(outputFloorplanDirectoryName + File.separator + TRANSPARENT_FILE_NAME);
BufferedImage baseImage = ImageIO.read(Files.newInputStream(Paths.get(outputFloorplanDirectoryName + File.separator + "base.png")));

for (String group : lightsGroups.keySet())
Expand Down Expand Up @@ -481,12 +506,26 @@ private String generateGroupRenders(String group, BufferedImage baseImage) throw
for (List<String> onLights : lightCombinations) {
String fileName = String.join("_", onLights) + ".png";
BufferedImage image = generateImage(onLights, outputRendersDirectoryName + File.separator + fileName);
generateFloorPlanImage(baseImage, image, outputFloorplanDirectoryName + File.separator + fileName);
yaml += generateLightYaml(groupLights, onLights, fileName);
boolean createOverlayImage = lightMixingMode == LightMixingMode.OVERLAY || (lightMixingMode == LightMixingMode.CSS && getEntityIsRgb(onLights.get(0)));
BufferedImage floorPlanImage = generateFloorPlanImage(baseImage, image, outputFloorplanDirectoryName + File.separator + fileName, createOverlayImage);
if (getEntityIsRgb(onLights.get(0))) {
String redTintedFileName = String.join("_", onLights) + ".red.png";
generateRedTintedImage(floorPlanImage, outputFloorplanDirectoryName + File.separator + redTintedFileName);
yaml += generateRgbLightYaml(onLights.get(0), fileName, redTintedFileName);
}
else
yaml += generateLightYaml(groupLights, onLights, fileName);
}
return yaml;
}

private void generateTransparentImage(String fileName) throws IOException {
BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
image.setRGB(0, 0, 0);
File imageFile = new File(fileName);
ImageIO.write(image, "png", imageFile);
}

private BufferedImage generateImage(List<String> onLights, String fileName) throws IOException, InterruptedException {
if (useExistingRenders && Files.exists(Paths.get(fileName))) {
propertyChangeSupport.firePropertyChange(Property.COMPLETED_RENDERS.name(), numberOfCompletedRenders, ++numberOfCompletedRenders);
Expand Down Expand Up @@ -529,12 +568,12 @@ private BufferedImage renderScene() throws IOException, InterruptedException {
return image;
}

private void generateFloorPlanImage(BufferedImage baseImage, BufferedImage image, String fileName) throws IOException {
private BufferedImage generateFloorPlanImage(BufferedImage baseImage, BufferedImage image, String fileName, boolean createOverlayImage) throws IOException {
File floorPlanFile = new File(fileName);

if (lightMixingMode != LightMixingMode.OVERLAY) {
if (!createOverlayImage) {
ImageIO.write(image, "png", floorPlanFile);
return;
return image;
}

BufferedImage overlay = new BufferedImage(baseImage.getWidth(), baseImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
Expand All @@ -545,7 +584,9 @@ private void generateFloorPlanImage(BufferedImage baseImage, BufferedImage image
overlay.setRGB(x, y, diff > sensitivity ? image.getRGB(x, y) : 0);
}
}

ImageIO.write(overlay, "png", floorPlanFile);
return overlay;
}

private int pixelDifference(int first, int second) {
Expand All @@ -556,6 +597,26 @@ private int pixelDifference(int first, int second) {
return diff / 3;
}

private BufferedImage generateRedTintedImage(BufferedImage image, String fileName) throws IOException {
File redTintedFile = new File(fileName);
BufferedImage tintedImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);

for(int x = 0; x < image.getWidth(); x++) {
for(int y = 0; y < image.getHeight(); y++) {
int rgb = image.getRGB(x, y);
if (rgb == 0)
continue;
Color original = new Color(rgb, true);
float hsb[] = Color.RGBtoHSB(original.getRed(), original.getGreen(), original.getBlue(), null);
Color redTint = Color.getHSBColor(1.0f, 0.75f, hsb[2]);
tintedImage.setRGB(x, y, redTint.getRGB());
}
}

ImageIO.write(tintedImage, "png", redTintedFile);
return tintedImage;
}

private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
private String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
Expand Down Expand Up @@ -613,6 +674,37 @@ conditions, entities, fileName, renderHash(fileName),
lightMixingMode == LightMixingMode.CSS ? " mix-blend-mode: lighten\n" : "");
}

private String generateRgbLightYaml(String lightName, String fileName, String redTintedFileName) throws IOException {
return String.format(
" - type: custom:config-template-card\n" +
" variables:\n" +
" LIGHT_STATE: states['%s'].state\n" +
" COLOR_MODE: states['%s'].attributes.color_mode\n" +
" LIGHT_COLOR: states['%s'].attributes.hs_color\n" +
" BRIGHTNESS: states['%s'].attributes.brightness\n" +
" entities:\n" +
" - %s\n" +
" element:\n" +
" type: image\n" +
" image: /local/floorplan/%s?version=%s\n" +
" state_image:\n" +
" 'on': >-\n" +
" ${COLOR_MODE === 'color_temp' || (COLOR_MODE === 'rgb' && LIGHT_COLOR && LIGHT_COLOR[0] == 0 && LIGHT_COLOR[1] == 0) ?\n" +
" '/local/floorplan/%s?version=%s' :\n" +
" '/local/floorplan/%s?version=%s' }\n" +
" entity: %s\n" +
" style:\n" +
" filter: '${ \"hue-rotate(\" + (LIGHT_COLOR ? LIGHT_COLOR[0] : 0) + \"deg)\"}'\n" +
" opacity: '${LIGHT_STATE === ''on'' ? (BRIGHTNESS / 255) : ''100''}'\n" +
" mix-blend-mode: lighten\n" +
" pointer-events: none\n" +
" left: 50%%\n" +
" top: 50%%\n" +
" width: 100%%\n",
lightName, lightName, lightName, lightName, lightName, TRANSPARENT_FILE_NAME, renderHash(TRANSPARENT_FILE_NAME),
fileName, renderHash(fileName), redTintedFileName, renderHash(redTintedFileName), lightName);
}

private void restoreLightsPower(Map<HomeLight, Float> lightsPower) {
for(HomeLight light : lightsPower.keySet()) {
light.setPower(lightsPower.get(light));
Expand Down
22 changes: 22 additions & 0 deletions src/com/shmuelzon/HomeAssistantFloorPlan/EntityOptionsPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ private enum ActionType {CLOSE, RESET_TO_DEFAULTS}
private JComboBox<Controller.EntityTapAction> tapActionComboBox;
private JLabel alwaysOnLabel;
private JCheckBox alwaysOnCheckbox;
private JLabel isRgbLabel;
private JCheckBox isRgbCheckbox;
private JButton closeButton;
private JButton resetToDefaultsButton;
private ResourceBundle resource;
Expand Down Expand Up @@ -118,6 +120,16 @@ public void actionPerformed(ActionEvent ev) {
}
});

isRgbLabel = new JLabel();
isRgbLabel.setText(resource.getString("HomeAssistantFloorPlan.Panel.isRgbLabel.text"));
isRgbCheckbox = new JCheckBox();
isRgbCheckbox.setSelected(controller.getEntityIsRgb(entityName));
isRgbCheckbox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ev) {
controller.setEntityIsRgb(entityName, isRgbCheckbox.isSelected());
}
});

closeButton = new JButton(actionMap.get(ActionType.CLOSE));
closeButton.setText(resource.getString("HomeAssistantFloorPlan.Panel.closeButton.text"));

Expand Down Expand Up @@ -160,6 +172,16 @@ private void layoutComponents() {
1, currentGridYIndex, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, insets, 0, 0));
currentGridYIndex++;

/* Is RGB */
add(isRgbLabel, new GridBagConstraints(
0, currentGridYIndex, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, insets, 0, 0));
isRgbLabel.setHorizontalAlignment(labelAlignment);
add(isRgbCheckbox, new GridBagConstraints(
1, currentGridYIndex, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, insets, 0, 0));
currentGridYIndex++;
}

public void displayView(Component parentComponent) {
Expand Down
8 changes: 6 additions & 2 deletions src/com/shmuelzon/HomeAssistantFloorPlan/Panel.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,13 +226,15 @@ public void mouseMoved(MouseEvent e) {
}
});
buildLightsGroupsTree(lightsGroups);
controller.addPropertyChangeListener(Controller.Property.NUMBER_OF_RENDERS, new PropertyChangeListener() {
PropertyChangeListener updateTreeOnProperyChanged = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
buildLightsGroupsTree(controller.getLightsGroups());
detectedLightsTree.repaint();
SwingUtilities.getWindowAncestor(Panel.this).pack();
}
});
};
controller.addPropertyChangeListener(Controller.Property.NUMBER_OF_RENDERS, updateTreeOnProperyChanged);
controller.addPropertyChangeListener(Controller.Property.ENTITY_ATTRIBUTE_CHANGED, updateTreeOnProperyChanged);
detectedLightsTree.putClientProperty("JTree.lineStyle", "Angled");
detectedLightsTree.setUI(new BasicTreeUI() {
@Override
Expand Down Expand Up @@ -560,6 +562,8 @@ private EntityNode generateLightEntityNode(String lightName) {

if (controller.getEntityAlwaysOn(lightName))
attributes.add(resource.getString("HomeAssistantFloorPlan.Panel.attributes.alwaysOn.text"));
if (controller.getEntityIsRgb(lightName))
attributes.add(resource.getString("HomeAssistantFloorPlan.Panel.attributes.isRgb.text"));

return new EntityNode(lightName, attributes);
}
Expand Down

0 comments on commit 1c52481

Please sign in to comment.