mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-12 13:21:53 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fcfd70fa7 | ||
|
|
1965f10584 | ||
|
|
ecb975540b |
@@ -1,195 +0,0 @@
|
||||
/**
|
||||
* Climax PSM-29ZBSR - Zigbee - Power Outlet Switch and Energy Meter
|
||||
*
|
||||
* Copyright 2015 SmartThings
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
* Thanks to SmartThings and its community!
|
||||
*
|
||||
* Product Page: http://www.climax.com.tw/psm29zb-zb.php
|
||||
* Developer manual https://fccid.io/document.php?id=2553357
|
||||
*
|
||||
*/
|
||||
metadata {
|
||||
definition (name: "Climax Power Outlet", namespace: "bortuzar", author: "bortuzar") {
|
||||
|
||||
capability "Actuator"
|
||||
capability "Configuration"
|
||||
capability "Refresh"
|
||||
capability "Sensor"
|
||||
capability "Switch"
|
||||
capability "Power Meter"
|
||||
|
||||
fingerprint endpointId: "0x0A", profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0702", outClusters: "0000"
|
||||
}
|
||||
|
||||
|
||||
// simulator metadata
|
||||
simulator {
|
||||
// status messages
|
||||
status "on": "on/off: 1"
|
||||
status "off": "on/off: 0"
|
||||
|
||||
// reply messages
|
||||
reply "zcl on-off on": "on/off: 1"
|
||||
reply "zcl on-off off": "on/off: 0"
|
||||
}
|
||||
|
||||
// UI tile definitions
|
||||
tiles {
|
||||
standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
|
||||
state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff"
|
||||
state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn"
|
||||
}
|
||||
standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
|
||||
state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
|
||||
}
|
||||
valueTile("power", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
state "power", label: '${currentValue} W'
|
||||
}
|
||||
|
||||
//standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") {
|
||||
// state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
|
||||
//}
|
||||
|
||||
|
||||
main(["switch", "power"])
|
||||
details(["switch", "power", "refresh"])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Parse incoming device messages to generate events
|
||||
def parse(String description) {
|
||||
|
||||
log.debug "Parse Method Called"
|
||||
log.trace description
|
||||
|
||||
if (description?.startsWith("catchall:")) {
|
||||
def msg = zigbee.parse(description)
|
||||
log.trace "data: $msg.data"
|
||||
|
||||
} else if (description?.startsWith("read attr -")) {
|
||||
def descMap = parseDescriptionAsMap(description)
|
||||
log.debug "Desc Map:"
|
||||
log.debug descMap
|
||||
|
||||
if (descMap.cluster == "0006" && descMap.attrId == "0000") {
|
||||
name = "switch"
|
||||
value = descMap.value.endsWith("01") ? "on" : "off"
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
} else if(descMap.cluster =="0702" && descMap.attrId == "0400") {
|
||||
|
||||
def value = convertHexToInt(descMap.value[-4..-1])/10
|
||||
// Reading the last 4 characters of the string...Maybe 4 are needed. Needs further test.
|
||||
// Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10
|
||||
log.debug value
|
||||
def name = "power"
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
} else {
|
||||
def name = description?.startsWith("on/off: ") ? "switch" : null
|
||||
def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null
|
||||
def result = createEvent(name: name, value: value)
|
||||
log.debug "Parse returned ${result?.descriptionText}"
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
def on() {
|
||||
log.debug "On Method called"
|
||||
sendEvent(name: "switch", value: "on")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 1 {}"
|
||||
}
|
||||
|
||||
def off() {
|
||||
log.debug "Off Method called"
|
||||
sendEvent(name: "switch", value: "off")
|
||||
"st cmd 0x${device.deviceNetworkId} 1 6 0 {}"
|
||||
}
|
||||
|
||||
def refresh() {
|
||||
log.debug "Refresh Method called";
|
||||
//read attribute 1024Decimal, translates to 400Hex.
|
||||
[
|
||||
"st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500",
|
||||
"st rattr 0x${device.deviceNetworkId} 1 0x0702 1024", , "delay 500"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def configure() {
|
||||
|
||||
log.debug "Configure Method called"
|
||||
|
||||
String zigbeeId = swapEndianHex(device.hub.zigbeeId)
|
||||
|
||||
def configCmds = [
|
||||
|
||||
//Switch Reporting
|
||||
"zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500",
|
||||
"send 0x${device.deviceNetworkId} 1 1", "delay 1000",
|
||||
|
||||
//bing to cluster 0x006. Switch On-Off
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000",
|
||||
|
||||
//bind to cluster 0x702. Power Consumption
|
||||
"zdo bind 0x${device.deviceNetworkId} 1 1 0x0702 {${device.zigbeeId}} {}", "delay 500",
|
||||
|
||||
]
|
||||
return configCmds + refresh() // send refresh cmds as part of config
|
||||
}
|
||||
|
||||
|
||||
def parseDescriptionAsMap(description) {
|
||||
(description - "read attr - ").split(",").inject([:]) { map, param ->
|
||||
def nameAndValue = param.split(":")
|
||||
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
|
||||
}
|
||||
}
|
||||
|
||||
private hex(value, width=2) {
|
||||
def s = new BigInteger(Math.round(value).toString()).toString(16)
|
||||
while (s.size() < width) {
|
||||
s = "0" + s
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
private Integer convertHexToInt(hex) {
|
||||
Integer.parseInt(hex,16)
|
||||
}
|
||||
|
||||
|
||||
private String swapEndianHex(String hex) {
|
||||
reverseArray(hex.decodeHex()).encodeHex()
|
||||
}
|
||||
|
||||
|
||||
private byte[] reverseArray(byte[] array) {
|
||||
int i = 0;
|
||||
int j = array.length - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
return array
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* Copyright 2015 Eurotronic
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
||||
* in compliance with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
|
||||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
|
||||
* for the specific language governing permissions and limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
metadata {
|
||||
definition (name: "Eurotronic Z-Wave Thermostatic Valve", namespace: "Eurotronic", author: "Eurotronic") {
|
||||
capability "Actuator"
|
||||
capability "Temperature Measurement"
|
||||
capability "Thermostat Heating Setpoint"
|
||||
capability "Thermostat Mode"
|
||||
capability "Thermostat Setpoint"
|
||||
capability "Sensor"
|
||||
capability "Switch Level"
|
||||
|
||||
command "energySave"
|
||||
command "manual"
|
||||
command "setWakeUpInterval", ["number"]
|
||||
command "setEnergySavingSetpoint", ["number"]
|
||||
|
||||
attribute "energySavingSetpoint", "number"
|
||||
|
||||
fingerprint deviceId:"0x0806", inClusters:"0x43, 0x40, 0x31, 0x20, 0x26, 0x77, 0x80, 0x84, 0x72, 0x86"
|
||||
}
|
||||
|
||||
tiles(scale: 1) {
|
||||
|
||||
valueTile("temperature", "device.temperature", width: 2, height: 2) {
|
||||
state("a", label:'${currentValue}°', icon: "st.Weather.weather2",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#153591"],
|
||||
[value: 7, color: "#1e9cbb"],
|
||||
[value: 15, color: "#90d2a7"],
|
||||
[value: 23, color: "#44b621"],
|
||||
[value: 29, color: "#f1d801"],
|
||||
[value: 35, color: "#d04e00"],
|
||||
]
|
||||
)
|
||||
}
|
||||
standardTile("mode", "device.thermostatMode") {
|
||||
state("off", label:'${name}', action:"heat", nextState:"heat", backgroundColor:"#808080", icon:"st.Seasonal Fall.seasonal-fall-008")
|
||||
state("heat", label:'${name}', action:"energySave", nextState:"energy save", backgroundColor:"#fdd470", icon:"st.Seasonal Winter.seasonal-winter-009")
|
||||
state("energy save", label:'save', action:"manual", nextState:"manual", backgroundColor:"#afd574", icon:"st.Transportation.transportation1")
|
||||
state("manual", label: '${name}', action:"off", nextState:"off", backgroundColor:"#8f00ff", icon:"st.Outdoor.outdoor18")
|
||||
}
|
||||
valueTile("battery", "device.battery", width: 1, height: 1) {
|
||||
state ("battery", label:'${currentValue}%', unit:"%", icon: "st.Health & Wellness.health9",
|
||||
backgroundColors:[
|
||||
[value: 0, color: "#ce1010"],
|
||||
[value: 50, color: "#3535ba"],
|
||||
[value: 100, color: "#0d740d"]
|
||||
]
|
||||
)
|
||||
}
|
||||
controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, range:"(13..50)") {
|
||||
state "a", action:"setHeatingSetpoint", backgroundColor:"#fdd470"
|
||||
}
|
||||
valueTile("heatingSetpoint", "device.heatingSetpoint") {
|
||||
state ("a", label:'${currentValue}°', backgroundColor:"#fdd470")
|
||||
}
|
||||
controlTile("energySavingControl", "device.energySavingSetpoint", "slider", height: 1, width: 2, range:"(13..50)") {
|
||||
state ("a", action:"setEnergySavingSetpoint", backgroundColor:"#afd574")
|
||||
}
|
||||
valueTile("energySavingSetpoint", "device.energySavingSetpoint" ) {
|
||||
state ("a", label:'${currentValue}°', backgroundColor:"#afd574")
|
||||
}
|
||||
controlTile("switchControlSlider", "device.switch", "slider", height: 1, width: 2) {
|
||||
state ("a", action:"setLevel", backgroundColor: "#8f00ff")
|
||||
}
|
||||
valueTile("switchValue", "device.switch") {
|
||||
state ("a", label:'${currentValue}%', backgroundColor:"#8f00ff")
|
||||
}
|
||||
|
||||
main "temperature"
|
||||
details(["temperature", "mode", "battery", "heatSliderControl", "heatingSetpoint", "energySavingControl", "energySavingSetpoint", "switchControlSlider", "switchValue"])
|
||||
}
|
||||
}
|
||||
|
||||
def parse(String description)
|
||||
{
|
||||
log.debug "Parse description: $description"
|
||||
|
||||
def result = []
|
||||
|
||||
if (state.wakeUpInterval != 240 || state.nodeid != zwaveHubNodeId) {
|
||||
result << response(zwave.wakeUpV2.wakeUpIntervalSet(nodeid: zwaveHubNodeId, seconds: 240))
|
||||
result << response(zwave.wakeUpV2.wakeUpIntervalGet().format())
|
||||
}
|
||||
|
||||
def zwe = []
|
||||
|
||||
if (description != "updated") {
|
||||
zwe = zwaveEvent(zwave.parse(description, [0x20: 1, 0x26: 3, 0x31: 4, 0x40: 2, 0x43: 2, 0x77: 1, 0x80: 1, 0x84: 2, 0x72: 1, 0x86: 1]))
|
||||
}
|
||||
|
||||
return result + zwe
|
||||
}
|
||||
|
||||
// Multilevel Switch
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd){
|
||||
log.debug "Switch multilevel get $cmd"
|
||||
|
||||
if (cmd.value == 255)
|
||||
cmd.value = 100
|
||||
|
||||
return createEvent(name: "switch", value: cmd.value)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd){
|
||||
log.debug "SwitchMultilevelStartLevelChange notification $cmd"
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd){
|
||||
log.debug "SwitchMultilevelStopLevelChange notification $cmd"
|
||||
}
|
||||
|
||||
//Multilevel Sensor
|
||||
def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv4.SensorMultilevelReport cmd){
|
||||
log.debug "Sensor multilevel get $cmd"
|
||||
def map = [:]
|
||||
if (cmd.sensorType == 1) {
|
||||
map.value = cmd.scaledSensorValue
|
||||
map.unit = cmd.scale
|
||||
map.name = "temperature"
|
||||
}
|
||||
return createEvent(map)
|
||||
}
|
||||
|
||||
//Thermostat
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd){
|
||||
log.debug "Thermostat mode notification $cmd"
|
||||
def map = [:]
|
||||
switch (cmd.mode) {
|
||||
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF:
|
||||
map.value = "off"
|
||||
break
|
||||
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT:
|
||||
map.value = "heat"
|
||||
break
|
||||
case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT:
|
||||
map.value = "energy save"
|
||||
break
|
||||
case 0x1F:
|
||||
map.value = "manual"
|
||||
break
|
||||
}
|
||||
map.name = "thermostatMode"
|
||||
return createEvent(map)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd){
|
||||
log.debug "ThermostatSetpointReport notification $cmd"
|
||||
|
||||
def map = [:]
|
||||
map.value = cmd.scaledValue
|
||||
map.unit = cmd.scale
|
||||
switch (cmd.setpointType) {
|
||||
case 1:
|
||||
map.name = "heatingSetpoint"
|
||||
break;
|
||||
case 11:
|
||||
map.name = "energySavingSetpoint"
|
||||
break;
|
||||
default:
|
||||
return [:]
|
||||
}
|
||||
// So we can respond with same format
|
||||
state.size = cmd.size
|
||||
state.scale = cmd.scale
|
||||
state.precision = cmd.precision
|
||||
return createEvent(map)
|
||||
}
|
||||
|
||||
//Battery
|
||||
def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd){
|
||||
log.debug "Battery notification $cmd"
|
||||
return createEvent(name: "battery", value: cmd.batteryLevel)
|
||||
}
|
||||
|
||||
//Wake up
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd){
|
||||
log.debug "WakeUpInterval notification $cmd"
|
||||
state.nodeid = cmd.nodeid
|
||||
state.wakeUpInterval = cmd.seconds
|
||||
return createEvent(name: "wakeUpIntervalSeconds", value: cmd.seconds)
|
||||
}
|
||||
|
||||
def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd){
|
||||
log.debug "Wake up notification $cmd $state"
|
||||
def result = []
|
||||
|
||||
if (state.modeCommand) {
|
||||
result << response(state.modeCommand)
|
||||
result << response(zwave.thermostatModeV2.thermostatModeGet().format())
|
||||
state.modeCommand = null
|
||||
}
|
||||
|
||||
if (state.setLevelCommand) {
|
||||
result << response(state.setLevelCommand)
|
||||
result << response(zwave.switchMultilevelV3.switchMultilevelGet().format())
|
||||
state.setLevelCommand = null
|
||||
}
|
||||
|
||||
if (state.setHeatingSetpointCommand) {
|
||||
result << response(state.setHeatingSetpointCommand)
|
||||
result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
|
||||
state.setHeatingSetpointCommand = null
|
||||
}
|
||||
|
||||
if (state.setEnergySavingSetpointCommand) {
|
||||
result << response(state.setEnergySavingSetpointCommand)
|
||||
result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 11).format())
|
||||
state.setEnergySavingSetpointCommand = null
|
||||
}
|
||||
|
||||
//For some reason response("delay 1200") is not functional which is really bad because hub is
|
||||
//not able to request all the information from device which it needs. All the commands are
|
||||
//sent at the same time.
|
||||
result << response(zwave.sensorMultilevelV4.sensorMultilevelGet().format())
|
||||
result << response(zwave.batteryV1.batteryGet().format())
|
||||
result << response(zwave.switchMultilevelV3.switchMultilevelGet().format())
|
||||
//result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format())
|
||||
//result << response(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 11).format())
|
||||
//result << response(zwave.thermostatModeV2.thermostatModeGet().format())
|
||||
result << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format())
|
||||
result
|
||||
}
|
||||
|
||||
//Unexpected
|
||||
def zwaveEvent(physicalgraph.zwave.Command cmd) {
|
||||
log.warn "Unexpected zwave command $cmd"
|
||||
}
|
||||
|
||||
// Command Implementations
|
||||
def off() {
|
||||
log.debug "Thermostat mode setting to off"
|
||||
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 0).format()
|
||||
}
|
||||
|
||||
def heat() {
|
||||
log.debug "Thermostat mode setting to heat"
|
||||
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 1).format()
|
||||
}
|
||||
|
||||
def energySave() {
|
||||
log.debug "Thermostat mode setting to energy save"
|
||||
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 11).format()
|
||||
}
|
||||
|
||||
def manual() {
|
||||
log.debug "Thermostat mode setting to manual"
|
||||
state.modeCommand = zwave.thermostatModeV2.thermostatModeSet(mode: 31).format()
|
||||
}
|
||||
|
||||
def setLevel(double lvl){
|
||||
//by Eurotronic specification 100% is 0xFF and 99% is 0x63
|
||||
if (lvl == 100.0)
|
||||
lvl = 255
|
||||
|
||||
log.debug "Changing switch level to $lvl"
|
||||
|
||||
state.setLevelCommand = zwave.switchMultilevelV3.switchMultilevelSet(value: 256).format()
|
||||
}
|
||||
|
||||
def setHeatingSetpoint(double sp){
|
||||
log.debug "Setting setpoint to $sp"
|
||||
|
||||
def map = [
|
||||
precision: 1,
|
||||
scale: 0,
|
||||
scaledValue: sp,
|
||||
setpointType: 1,
|
||||
size: 2,
|
||||
]
|
||||
|
||||
state.setHeatingSetpointCommand = zwave.thermostatSetpointV2.thermostatSetpointSet(map).format()
|
||||
}
|
||||
|
||||
def setEnergySavingSetpoint(double sp){
|
||||
log.debug "Setting energy saving setpoint to $sp"
|
||||
|
||||
def map = [
|
||||
precision: 1,
|
||||
scale: 0,
|
||||
scaledValue: sp,
|
||||
setpointType: 11,
|
||||
size: 2,
|
||||
]
|
||||
|
||||
state.setEnergySavingSetpointCommand = zwave.thermostatSetpointV2.thermostatSetpointSet(map).format()
|
||||
}
|
||||
@@ -50,7 +50,7 @@ preferences {
|
||||
}
|
||||
section("Send Notifications?") {
|
||||
input("recipients", "contact", title: "Send notifications to") {
|
||||
input "phone", "phone", title: "Send an SMS to this number?"
|
||||
input "phone", "phone", title: "Send an SMS to this number?", required:false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +266,9 @@ def sendAway(msg) {
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
sendSms(phone, msg)
|
||||
if(phone){
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +282,9 @@ def sendHome(msg) {
|
||||
}
|
||||
else {
|
||||
sendPush(msg)
|
||||
sendSms(phone, msg)
|
||||
if(phone){
|
||||
sendSms(phone, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,4 +343,4 @@ private getTimeIntervalLabel() {
|
||||
|
||||
private hideOptionsSection() {
|
||||
(starting || ending || days || modes) ? false: true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user