Lovelace Custom Fan Card Example

Made some more changes to the style using a flex container and now the buttons do not overlap when resizing. Still need to figure out how to make them smaller

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style>
            .flex-container {
              display: flex;
              justify-content: center;
              align-items: center;
            }

            .flex-container > div {
              margin: 0px;
              padding: 1px;
            }
            </style>

            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='flex-container' on-click="stopPropagation">
                   <div><mwc-button
                        raised noink name="off"
                        on-click='setSpeed'
                        disabled='[[_isOff]]'>Off</mwc-button></div> 
                    <div><mwc-button
                        raised noink name="low"
                        on-click='setSpeed'
                        disabled='[[_isLowSpeed]]'>Lo</mwc-button></div>
                    <div><mwc-button
                        raised noink name="medium"
                        on-click='setSpeed'
                        disabled='[[_isMedSpeed]]'>Med</mwc-button></div>
                    <div><mwc-button
                        raised noink name="high"
                        on-click='setSpeed'
                        disabled='[[_isHghSpeed]]'>Hi</mwc-button></div>
                </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _isOff: Boolean,
            _isLowSpeed: Boolean,
            _isMedSpeed: Boolean,
            _isHghSpeed: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }

        this.setProperties({
            _stateObj: stateObj,
            _isOff: stateObj.state  === 'off',
            _isLowSpeed: speed === 'low' && stateObj.state === 'on',
            _isMedSpeed: speed === 'medium' && stateObj.state === 'on',
            _isHghSpeed: speed == 'high' && stateObj.state === 'on'
        });
    }

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

image

image

Can make them thinner using dense=""
image

Thanks for sharing. Came out like that on mine.

Well, after many, many hours trying to figure out why none of the regular styling options work on the new mwc-button I think (maybe…) I figured out what is happening. I’m not a web developer so take this for the value in what you paid for it…:wink:

It looks to me like when the new mwc-button was implemented it created a thing called a “shadowDOM” that locks up the available style options as soon as the “mwc-button” is called in the code. Looking thru the dev-console the values for things like text size, text color, and background colors (along with a bunch of other stuff) are fixed by the shadowDOM that gets imported when the mdc-button class is used. From what I have learned there is no way to turn it off either.

But, I learned in my digging around there might be a way to override those values by creating a new shadowDOM later in the code but I don’t understand web development enough to figure out how to do that.

Unless someone smarter than me can figure out how to override the imposed style then I think we’re stuck with the way it looks above. Or I’ll have to move to a different method of button. That really sucks because I never like any of the other button options available. To me they all looked way too “blocky”. Kind of like these do now. :slightly_frowning_face:

Drat!!

I might put in a bug report to see if the restrictions could be removed but I may be in the dog house with the developers right now so maybe someone else might get better traction doing it.

That looks right. I’m using a theme so my colours are a little different.

What did you change in the theme to get the button colors to be different?

I’m using the clear theme.

https://community.home-assistant.io/t/clear-theme/100464

Thanks.

It looks like it’s using the primary-color for the dark (not-disabled) button color. I can’t even figure how to split that out and make it individually customizable. there is a reference to “mdc-theme-primary” in the home-assistant-polymer github style page which I thought should have selected the color for the buttons but I tried that and it didn’t work either.

Maybe this can help

It might if I knew what to do with it! :grinning:

I’m not sure from that if they fixed it everywhere in HA or just put that work around in for the button card. I’m on V88.1 now and it looks like it’s still broken for HA in general.

I posted an issue on the HA-polymer github so maybe it will get addressed.

UPDATE:

Balloob answered on the github issue and said that there was nothing that could be done about the mwc-button issue since it was maintained by a third party but he made a suggestion that, thinking a little bit about it, is now obvious - just use a regular button. Duh!

So with that in mind here is the code:

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
            <style>
                :host {
                    line-height: inherit;
                }
                .speed {
                    min-width: 30px;
                    max-width: 30px;
                    height: 30px;
                    margin-left: 2px;
                    margin-right: 2px;
                    background-color:#759aaa;
	                border: 1px solid lightgrey; 
                    border-radius: 4px;
	                font-size: 10px !important;
                    color: inherit;
                    text-align: center;
	                float: right !important;
                    padding: 1px;
		    }
				
            </style>
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='horizontal justified layout' on-click="stopPropagation">
                    <button
                            class='speed'
                            style='[[_lowOnColor]]'
                            toggles name="low"
                            on-click='setSpeed'
                            disabled='[[_isOnLow]]'>LOW</button>
                    <button
                            class='speed'
                            style='[[_medOnColor]]'
                            toggles name="medium"
                            on-click='setSpeed'
                            disabled='[[_isOnMed]]'>MED</button>
                    <button
                            class='speed'
                            style='[[_highOnColor]]'
                            toggles name="high"
                            on-click='setSpeed'
                            disabled='[[_isOnHigh]]'>HIGH</button>
                    <button
                            class='speed'
                            style='[[_offColor]]'
                            toggles name="off"
                            on-click='setSpeed'
                            disabled='[[_isOffState]]'>OFF</button>
                    </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _lowOnColor: String,
            _medOnColor: String,
            _highOnColor: String,
            _offColor: String,
            _isOffState: Boolean,
            _isOnState: Boolean,
            _isOnLow: Boolean,
            _isOnMed: Boolean,
            _isOnHigh: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }
		
        let low;
	    let med;
	    let high;
	    let offstate;
		
	    if (stateObj && stateObj.attributes) {
	        if (stateObj.state == 'on' && stateObj.attributes.speed == 'low') {
		        low = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'medium') {
		        med = 'on';
		} else if (stateObj.state == 'on' && stateObj.attributes.speed == 'high') {
		        high = 'on';
		} else {
			offstate = 'on';
		}
	}
		
        let lowcolor;
        let medcolor;
	    let hicolor;
	    let offcolor;
		
	    if (low == 'on') {
            lowcolor = 'background-color: #43A047';
	    } else { 
            lowcolor = '';
	    }
	
        if (med == 'on') {
            medcolor = 'background-color: #43A047';
	    } else {
            medcolor = '';
	    }
	
	    if (high == 'on') {
            hicolor = 'background-color: #43A047';
	    } else {
            hicolor = '';
	    }
		
	    if (offstate == 'on') {
            //offcolor = 'background-color: #43A047';
            offcolor = 'background-color: #f44c09';
	    } else {
            offcolor = '';
	    }
		
	this.setProperties({
        _stateObj: stateObj,
	    _isOffState: stateObj.state == 'off',
        _isOnLow: low === 'on',
	    _isOnMed: med === 'on',
	    _isOnHigh: high === 'on',
	    _lowOnColor: lowcolor,
	    _medOnColor: medcolor,
	    _highOnColor: hicolor,
	    _offColor: offcolor
    });
}

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

and the result:

ex

the only difference I can see between this one and the previous paper-button is that the text on the disabled button doesn’t grey out. Not a big deal for me since I use the colors to see the state.

1 Like

Building from @slipx06 and @finity, I switched to a button, then copied out all the shadow dom css from mwc-button into this element. I haven’t tried, but I’m hoping it’ll pick up most theme changes this way.

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style is="custom-style" include="iron-flex iron-flex-alignment"></style>
            <style>
                .flex-container {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                }
                @keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-ripple-surface--test-edge-var-bug{--mdc-ripple-surface-test-edge-var: 1px solid #000;visibility:hidden}.mdc-ripple-surface--test-edge-var-bug::before{border:var(--mdc-ripple-surface-test-edge-var)}.mdc-button{font-family:Roboto,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-size:.875rem;line-height:2.25rem;font-weight:500;letter-spacing:.0892857143em;text-decoration:none;text-transform:uppercase;--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;padding:0 8px 0 8px;display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;height:36px;border:none;outline:none;line-height:inherit;user-select:none;-webkit-appearance:none;overflow:hidden;vertical-align:middle;border-radius:4px}.mdc-button::before,.mdc-button::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-button::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-button.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-button.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-button.mdc-ripple-upgraded--foreground-activation::after{animation:225ms mdc-ripple-fg-radius-in forwards,75ms mdc-ripple-fg-opacity-in forwards}.mdc-button.mdc-ripple-upgraded--foreground-deactivation::after{animation:150ms mdc-ripple-fg-opacity-out;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button::before,.mdc-button::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-button.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{background-color:transparent;color:rgba(0,0,0,.37);cursor:default;pointer-events:none}.mdc-button.mdc-button--dense{border-radius:4px}.mdc-button:not(:disabled){background-color:transparent}.mdc-button:not(:disabled){color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-button::before,.mdc-button::after{background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button::before,.mdc-button::after{background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button:hover::before{opacity:.04}.mdc-button:not(.mdc-ripple-upgraded):focus::before,.mdc-button.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.12}.mdc-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.16}.mdc-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.16}.mdc-button .mdc-button__icon{margin-left:0;margin-right:8px;display:inline-block;width:18px;height:18px;font-size:18px;vertical-align:top}[dir=rtl] .mdc-button .mdc-button__icon,.mdc-button .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:0}.mdc-button svg.mdc-button__icon{fill:currentColor}.mdc-button--raised .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon,.mdc-button--outlined .mdc-button__icon{margin-left:-4px;margin-right:8px}[dir=rtl] .mdc-button--raised .mdc-button__icon,.mdc-button--raised .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__icon,.mdc-button--outlined .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:-4px}.mdc-button--raised,.mdc-button--unelevated{padding:0 16px 0 16px}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.37)}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:#fff}@supports not (-ms-ime-align: auto){.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:var(--mdc-theme-on-primary, #fff)}}.mdc-button--raised:hover::before,.mdc-button--unelevated:hover::before{opacity:.08}.mdc-button--raised:not(.mdc-ripple-upgraded):focus::before,.mdc-button--raised.mdc-ripple-upgraded--background-focused::before,.mdc-button--unelevated:not(.mdc-ripple-upgraded):focus::before,.mdc-button--unelevated.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.24}.mdc-button--raised:not(.mdc-ripple-upgraded)::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button--raised:not(.mdc-ripple-upgraded):active::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.32}.mdc-button--raised.mdc-ripple-upgraded,.mdc-button--unelevated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.32}.mdc-button--raised{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-button--raised:hover,.mdc-button--raised:focus{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-button--raised:active{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-button--raised:disabled{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-button--outlined{border-style:solid;padding:0 14px 0 14px;border-width:2px}.mdc-button--outlined:disabled{border-color:rgba(0,0,0,.37)}.mdc-button--outlined:not(:disabled){border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-button--dense{height:32px;font-size:.8125rem}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased}:host{display:inline-flex;outline:none}.mdc-button{flex:1}
                :host {
                    display: inherit;
                }
                .speeds button:host {
                    display: inline-flex;
                    outline: none;
                }
                .speeds button {
                    min-width: 34px !important;
                    width: 34px;
                }
                /*ha-entity-toggle {
                    margin-left: 16px;
                }*/
            </style>
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='flex-container' on-click="_stopPropagation">
                    <div class="speeds">
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="off"
                            on-click='_setSpeed'
                            disabled='[[_isOff]]'>
                            <span class="mdc-button__label">OFF</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="low"
                            on-click='_setSpeed'
                            disabled='[[_isLowSpeed]]'>
                            <span class="mdc-button__label">LO</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="medium"
                            on-click='_setSpeed'
                            disabled='[[_isMedSpeed]]'>
                            <span class="mdc-button__label">MED</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="high"
                            on-click='_setSpeed'
                            disabled='[[_isHghSpeed]]'>
                            <span class="mdc-button__label">HI</span>
                        </button>
                    </div>
                    <!--<ha-entity-toggle hass="[[hass]]" state-obj="[[_stateObj]]"></ha-entity-toggle>-->
                </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: '_hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _isOff: Boolean,
            _isLowSpeed: Boolean,
            _isMedSpeed: Boolean,
            _isHghSpeed: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    _hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }

        this.setProperties({
            _stateObj: stateObj,
            _isOff: speed === 'off',
            _isLowSpeed: speed == 'low',
            _isMedSpeed: speed === 'medium',
            _isHghSpeed: speed === 'high'
        });
    }

    _stopPropagation(e) {
        e.stopPropagation();
    }

    _setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }

}

customElements.define('custom-fan-card', CustomFanCard);

and the result:

image

image

2 Likes

It looks good. I love a good group effort that gets us all where we want to end up! :slightly_smiling_face:

@rhodges Thank you. This is my updated code based on some of your changes.

class CustomFanCard extends Polymer.Element {

    static get template() {
        return Polymer.html`
            <style>
                .flex-container {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                }
                @keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}.mdc-ripple-surface--test-edge-var-bug{--mdc-ripple-surface-test-edge-var: 1px solid #000;visibility:hidden}.mdc-ripple-surface--test-edge-var-bug::before{border:var(--mdc-ripple-surface-test-edge-var)}.mdc-button{font-family:Roboto,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-size:.875rem;line-height:2.25rem;font-weight:500;letter-spacing:.0892857143em;text-decoration:none;text-transform:uppercase;--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;padding:0 8px 0 8px;display:inline-flex;position:relative;align-items:center;justify-content:center;box-sizing:border-box;min-width:64px;height:36px;border:none;outline:none;line-height:inherit;user-select:none;-webkit-appearance:none;overflow:hidden;vertical-align:middle;border-radius:4px}.mdc-button::before,.mdc-button::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-button::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1}.mdc-button.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-button.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-button.mdc-ripple-upgraded--foreground-activation::after{animation:225ms mdc-ripple-fg-radius-in forwards,75ms mdc-ripple-fg-opacity-in forwards}.mdc-button.mdc-ripple-upgraded--foreground-deactivation::after{animation:150ms mdc-ripple-fg-opacity-out;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-button::before,.mdc-button::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-button.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-button::-moz-focus-inner{padding:0;border:0}.mdc-button:active{outline:none}.mdc-button:hover{cursor:pointer}.mdc-button:disabled{background-color:transparent;color:rgba(0,0,0,.37);cursor:default;pointer-events:none}.mdc-button.mdc-button--dense{border-radius:4px}.mdc-button:not(:disabled){background-color:transparent}.mdc-button:not(:disabled){color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}.mdc-button::before,.mdc-button::after{background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button::before,.mdc-button::after{background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button:hover::before{opacity:.04}.mdc-button:not(.mdc-ripple-upgraded):focus::before,.mdc-button.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.12}.mdc-button:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.16}.mdc-button.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.16}.mdc-button .mdc-button__icon{margin-left:0;margin-right:8px;display:inline-block;width:18px;height:18px;font-size:18px;vertical-align:top}[dir=rtl] .mdc-button .mdc-button__icon,.mdc-button .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:0}.mdc-button svg.mdc-button__icon{fill:currentColor}.mdc-button--raised .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon,.mdc-button--outlined .mdc-button__icon{margin-left:-4px;margin-right:8px}[dir=rtl] .mdc-button--raised .mdc-button__icon,.mdc-button--raised .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--unelevated .mdc-button__icon,.mdc-button--unelevated .mdc-button__icon[dir=rtl],[dir=rtl] .mdc-button--outlined .mdc-button__icon,.mdc-button--outlined .mdc-button__icon[dir=rtl]{margin-left:8px;margin-right:-4px}.mdc-button--raised,.mdc-button--unelevated{padding:0 16px 0 16px}.mdc-button--raised:disabled,.mdc-button--unelevated:disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.37)}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:#6200ee}@supports not (-ms-ime-align: auto){.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){background-color:var(--mdc-theme-primary, #6200ee)}}.mdc-button--raised:not(:disabled),.mdc-button--unelevated:not(:disabled){color:#fff;color:var(--mdc-theme-on-primary, #fff)}.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:#fff}@supports not (-ms-ime-align: auto){.mdc-button--raised::before,.mdc-button--raised::after,.mdc-button--unelevated::before,.mdc-button--unelevated::after{background-color:var(--mdc-theme-on-primary, #fff)}}.mdc-button--raised:hover::before,.mdc-button--unelevated:hover::before{opacity:.08}.mdc-button--raised:not(.mdc-ripple-upgraded):focus::before,.mdc-button--raised.mdc-ripple-upgraded--background-focused::before,.mdc-button--unelevated:not(.mdc-ripple-upgraded):focus::before,.mdc-button--unelevated.mdc-ripple-upgraded--background-focused::before{transition-duration:75ms;opacity:.24}.mdc-button--raised:not(.mdc-ripple-upgraded)::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-button--raised:not(.mdc-ripple-upgraded):active::after,.mdc-button--unelevated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:.32}.mdc-button--raised.mdc-ripple-upgraded,.mdc-button--unelevated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity: 0.32}.mdc-button--raised{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-button--raised:hover,.mdc-button--raised:focus{box-shadow:0px 2px 4px -1px rgba(0, 0, 0, 0.2),0px 4px 5px 0px rgba(0, 0, 0, 0.14),0px 1px 10px 0px rgba(0,0,0,.12)}.mdc-button--raised:active{box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12)}.mdc-button--raised:disabled{box-shadow:0px 0px 0px 0px rgba(0, 0, 0, 0.2),0px 0px 0px 0px rgba(0, 0, 0, 0.14),0px 0px 0px 0px rgba(0,0,0,.12)}.mdc-button--outlined{border-style:solid;padding:0 14px 0 14px;border-width:2px}.mdc-button--outlined:disabled{border-color:rgba(0,0,0,.37)}.mdc-button--outlined:not(:disabled){border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-button--dense{height:32px;font-size:.8125rem}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-feature-settings:"liga";-webkit-font-smoothing:antialiased}:host{display:inline-flex;outline:none}.mdc-button{flex:1}
                :host {
                    display: inherit;
                }
                .speeds button:host {
                    display: inline-flex;
                    outline: none;
                }
                .speeds button {
                    min-width: 34px !important;
                    width: 34px;
                    font-size: 11px !important;
                }
            </style>
  
            <hui-generic-entity-row hass="[[hass]]" config="[[_config]]">
                <div class='flex-container' on-click="stopPropagation">
                   <div class="speeds">
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="off"
                            on-click='setSpeed'
                            disabled='[[_isOff]]'>
                            <span class="mdc-button__label">OFF</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="low"
                            on-click='setSpeed'
                            disabled='[[_isLowSpeed]]'>
                            <span class="mdc-button__label">LO</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="medium"
                            on-click='setSpeed'
                            disabled='[[_isMedSpeed]]'>
                            <span class="mdc-button__label">MED</span>
                        </button>
                        <button
                            class="mdc-button mdc-button--raised mdc-ripple-upgraded"
                            toggles name="high"
                            on-click='_setSpeed'
                            disabled='[[_isHghSpeed]]'>
                            <span class="mdc-button__label">HI</span>
                        </button>
                    </div>
            </hui-generic-entity-row>
        `;
    }

    static get properties() {
        return {
            hass: {
                type: Object,
                observer: 'hassChanged'
            },
            _config: Object,
            _stateObj: Object,
            _isOff: Boolean,
            _isLowSpeed: Boolean,
            _isMedSpeed: Boolean,
            _isHghSpeed: Boolean
        }
    }

    setConfig(config) {
        this._config = config;
    }

    hassChanged(hass) {

        const config = this._config;
        const stateObj = hass.states[config.entity];

        let speed;
        if (stateObj && stateObj.attributes) {
            speed = stateObj.attributes.speed || 'off';
        }

        this.setProperties({
            _stateObj: stateObj,
            _isOff: stateObj.state  === 'off',
            _isLowSpeed: speed === 'low' && stateObj.state === 'on',
            _isMedSpeed: speed === 'medium' && stateObj.state === 'on',
            _isHghSpeed: speed == 'high' && stateObj.state === 'on'
        });
    }

    stopPropagation(e) {
        e.stopPropagation();
    }

    setSpeed(e) {
        const speed = e.currentTarget.getAttribute('name');
        this.hass.callService('fan', 'set_speed', {
            entity_id: this._config.entity, speed: speed
        });
    }
}

customElements.define('custom-fan-card', CustomFanCard);

image

image

2 Likes

I vote this card gets moved in the core repo… :grin: Very nice work guys

Which one? There’s at least three versions right now in this thread for v88 alone! :laughing:

I vote for mine. It has more pretty colors. :heart_eyes:

@slipx06 @finity

Great card guys. Using 88.1 here and working great with slipx06’s latest posted code. The only change I’d like to make is to make the selected option highlighted with the rest greyed out. Any chance you point me to where in the code I could change this?

This is definity worthy of a github repo! Would be great to get added to the custom updater/tracker card too. :slightly_smiling_face:

the code that I’m using is quite a bit different than the other one. Maybe I need to split it out of here and call it something else to minimize any confusion.

But if you use the code for my card you can make the on and off colors anything you want.

to set the on color your change the number after the # in these lines:

           if (low == 'on') {
			lowcolor = 'background-color: #43A047';
		} else {
			lowcolor = '';
		}
		
		if (med == 'on') {
			medcolor = 'background-color: #43A047';
		} else {
			medcolor = '';
		}
		
		if (high == 'on') {
			hicolor = 'background-color: #43A047';
		} else {
			hicolor = '';
		}
		
		if (offstate == 'on') {
			offcolor = 'background-color: #f44c09';
		} else {
			offcolor = '';
		}

then to change the off color just change the "background: " value under the “.speed” style section.

Using the other code I pretty sure you don’t have a choice in the colors. It just follows your theme.

Sorry @finity, I should have mentioned I did see those css settings in your code. I can basically follow your code but I really can’t make heads or tails of what is happening in slip06’s version.
Your example there is the result I was trying to achieve though!

I actually chose to use @slipx06’s code because it does follow the theme but with all the themes I tried it kinda looks like the ‘on’ option is actually the one that is off. On some themes the ‘on’ option basically disappears. For example, try the “Halloween” theme from the community themes package (not that I would ever actually use this one myself.).

I think this issue could be fixed by using a standard ‘greyed out’ disabled colour for the off options and using the theme colour for the selected or ‘on’ option. If I had any clue where to accomplish this I would just do it myself but like I said, it’s gobbledegook to me.

Also, perhaps there is a way to put a flag/option in the code somewhere so the user has a choice to use the theme or set custom colours as an option in the lovelace ui? Is that even possible? Just a thought.

I will note that one issue using your version is that one would have to go back and change the colour back every time the js file gets updated.

Either way, I’m grateful for the card. It solves one of my long standing irritants with my system, so thank you!

I don’t think that is something you’ll have to worry about. Unless there is a change to the HA base code that requires this card to be forced to change (like what happened this time) then I don’t plan on modifying it. And since it doesn’t use the custom updater there is no way for it to automatically update your card anyway. So you have total control over what you do with the card. I kind of like that. :wink:

EDIT: unless, of course, the common name we are all using “custom-fan-card” gets added to the custom updater by someone. Then I guess that’s another reason why I should change the name of my card to something else. :thinking:

IMHO that is a great reason to utilize something like the custom updater. You never know what’s going to happen, especially with a developing platform like Home Assistant.

For people for whom HA isn’t their primary hobby it does make keeping track of things much simpler! Heck even my utterly technically illiterate wife can handle updating everything when it’s a one click solution. :joy: